details:   https://code.tryton.org/hatch-tryton/commit/4fdd86f378e3
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Mar 11 23:29:20 2026 +0100
description:
        Initial commit
diffstat:

 .flake8                             |    2 +
 .gitlab-ci.yml                      |   66 +++++++++
 .hgignore                           |    7 +
 COPYRIGHT                           |    2 +
 LICENSE                             |   19 ++
 README.rst                          |    5 +
 hatch_tryton/__init__.py            |    4 +
 hatch_tryton/hooks.py               |   11 +
 hatch_tryton/plugin.py              |  114 +++++++++++++++
 hatch_tryton/tests/__init__.py      |    2 +
 hatch_tryton/tests/test_metadata.py |  258 ++++++++++++++++++++++++++++++++++++
 hatch_tryton/tests/tools.py         |   14 +
 pyproject.toml                      |   39 +++++
 tox.ini                             |   14 +
 14 files changed, 557 insertions(+), 0 deletions(-)

diffs (613 lines):

diff -r 000000000000 -r 4fdd86f378e3 .flake8
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/.flake8   Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,2 @@
+[flake8]
+ignore=E123,E124,E126,E128,W503
diff -r 000000000000 -r 4fdd86f378e3 .gitlab-ci.yml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/.gitlab-ci.yml    Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,66 @@
+workflow:
+  rules:
+    - if: $CI_COMMIT_BRANCH =~ /^topic\/.*/ && $CI_PIPELINE_SOURCE == "push"
+      when: never
+    - when: always
+
+stages:
+  - check
+  - test
+
+.check:
+  stage: check
+  image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/tryton/ci
+
+check-flake8:
+  extends: .check
+  script:
+    - flake8
+
+check-isort:
+  extends: .check
+  script:
+    - isort -m VERTICAL_GRID -c .
+
+check-dist:
+  extends: .check
+  before_script:
+    - pip install build twine
+  script:
+    - pyproject-build
+    - twine check dist/*
+
+.test:
+  stage: test
+
+.test-tox:
+  extends: .test
+  variables:
+    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+  cache:
+    paths:
+      - .cache/pip
+  before_script:
+    - pip install tox
+  coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
+  artifacts:
+    reports:
+      junit: junit.xml
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage.xml
+
+test-tox-python:
+  extends: .test-tox
+  image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:${PYTHON_VERSION}
+  script:
+    - tox -e "py${PYTHON_VERSION/./}" -- -v --output-file junit.xml
+  parallel:
+    matrix:
+      - PYTHON_VERSION: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+
+test-tox-pypy:
+  extends: .test-tox
+  image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/pypy:3
+  script:
+    - tox -e pypy3 -- -v --output-file junit.xml
diff -r 000000000000 -r 4fdd86f378e3 .hgignore
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,7 @@
+syntax: glob
+*.py[cdo]
+*.egg-info
+dist/
+build/
+.tox/
+.coverage
diff -r 000000000000 -r 4fdd86f378e3 COPYRIGHT
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYRIGHT Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,2 @@
+Copyright (C) 2026 Cédric Krier.
+Copyright (C) 2026 B2CK SPRL.
diff -r 000000000000 -r 4fdd86f378e3 LICENSE
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE   Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,19 @@
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff -r 000000000000 -r 4fdd86f378e3 README.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/README.rst        Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,5 @@
+============
+hatch-tryton
+============
+
+A ``hatchling`` plugin to manage Tryton dependencies.
diff -r 000000000000 -r 4fdd86f378e3 hatch_tryton/__init__.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/hatch_tryton/__init__.py  Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,4 @@
+# This file is part of hatch-tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+__version__ = '0.0.1'
diff -r 000000000000 -r 4fdd86f378e3 hatch_tryton/hooks.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/hatch_tryton/hooks.py     Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,11 @@
+# This file is part of hatch-tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from hatchling.plugin import hookimpl
+
+from .plugin import TrytonMetadataHook
+
+
+@hookimpl
+def hatch_register_metadata_hook():
+    return TrytonMetadataHook
diff -r 000000000000 -r 4fdd86f378e3 hatch_tryton/plugin.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/hatch_tryton/plugin.py    Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,114 @@
+# This file is part of hatch-tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+import re
+from collections import namedtuple
+from configparser import ConfigParser
+from pathlib import Path
+from typing import Optional
+
+from hatchling.metadata.plugin.interface import MetadataHookInterface
+
+TrytonConfig = namedtuple('TrytonConfig', ['version', 'depends'])
+
+
+def _get_tryton_cfg(
+        root: str, config: Optional[str] = None) -> Optional[TrytonConfig]:
+    tryton_path = Path(root) / (config or 'tryton.cfg')
+    if tryton_path.exists():
+        config = ConfigParser()
+        with tryton_path.open() as f:
+            config.read_file(f)
+        return TrytonConfig(
+            version=config.get('tryton', 'version'),
+            depends=config.get(
+                'tryton', 'depends', fallback='').strip().splitlines(),
+            )
+
+
+def _get_series(version: str) -> tuple[int, int]:
+    return tuple(map(int, version.split('.', 2)[:2]))
+
+
+def _set_version(name: str, series: tuple[int, int]) -> str:
+    major, minor = series
+    return f'{name} >= {major}.{minor}, < {major}.{minor + 1}'
+
+
+def _package_name(module: str, prefixes: dict[str, str]) -> str:
+    prefix = prefixes.get(module, 'trytond')
+    return f'{prefix}_{module}'
+
+
+def _get_readme(root: str, path: str) -> dict[str, str]:
+    readme_path = Path(root) / path
+    text = readme_path.read_text()
+    if readme_path.suffix == '.rst':
+        text = re.sub(
+            r'(?m)^\.\. toctree::\r?\n((^$|^\s.*$)\r?\n)*', '', text)
+        content_type = 'text/x-rst'
+    elif readme_path.suffix == '.md':
+        content_type = 'text/markdown'
+    else:
+        raise ValueError(f"unsupported readme suffix {readme_path.suffix!r}")
+    return {
+        'content-type': content_type,
+        'text': text,
+        }
+
+
+class TrytonMetadataHook(MetadataHookInterface):
+    PLUGIN_NAME = 'tryton'
+
+    def update(self, metadata: dict) -> None:
+        tryton_cfg = _get_tryton_cfg(self.root, self.config.get('config'))
+        prefixes = self.config.get('prefixes', {})
+
+        dependencies = self.config.get('dependencies', [])
+
+        if tryton_cfg:
+            series = _get_series(tryton_cfg.version)
+        elif metadata.get('version'):
+            series = _get_series(metadata['version'])
+        else:
+            series = (1, 0)
+
+        if tryton_dependencies := self.config.get('tryton_dependencies', []):
+            for package in tryton_dependencies:
+                dependencies.append(_set_version(package, series))
+
+        if tryton_cfg:
+            if 'version' in metadata.get('dynamic', []):
+                metadata['version'] = tryton_cfg.version
+
+            for module in tryton_cfg.depends:
+                package = _package_name(module, prefixes)
+                dependencies.append(_set_version(package, series))
+
+        if dependencies:
+            if ('dependencies' in metadata
+                    or 'dependencies' not in metadata.get('dynamic', [])):
+                raise ValueError("'dependencies' must be dynamic")
+            metadata['dependencies'] = dependencies
+
+        optional_dependencies = self.config.get('optional-dependencies', {})
+
+        if tryton_optional_dependencies := self.config.get(
+                'tryton-optional-dependencies', {}):
+            for option, packages in tryton_optional_dependencies.items():
+                o_dep = optional_dependencies.setdefault(option, [])
+                for package in packages:
+                    o_dep.append(_set_version(package, series))
+
+        if optional_dependencies:
+            if ('optional-dependencies' in metadata
+                    or 'optional-dependencies' not in metadata.get(
+                        'dynamic', [])):
+                raise ValueError("'optional-dependencies' must be dynamic")
+            metadata['optional-dependencies'] = optional_dependencies
+
+        if readme := self.config.get('readme'):
+            if ('readme' in metadata
+                    or 'readme' not in metadata.get('dynamic', [])):
+                raise ValueError("'readme' must be dynamic")
+            metadata['readme'] = _get_readme(self.root, readme)
diff -r 000000000000 -r 4fdd86f378e3 hatch_tryton/tests/__init__.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/hatch_tryton/tests/__init__.py    Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,2 @@
+# This file is part of hatch-tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
diff -r 000000000000 -r 4fdd86f378e3 hatch_tryton/tests/test_metadata.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/hatch_tryton/tests/test_metadata.py       Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,258 @@
+# This file is part of hatch-tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+import unittest
+from textwrap import dedent
+from typing import TYPE_CHECKING
+
+from hatch_tryton.plugin import TrytonMetadataHook
+
+from .tools import with_temporay_directory
+
+if TYPE_CHECKING or True:
+    from pathlib import Path
+
+
+class TestMetadata(unittest.TestCase):
+
+    @with_temporay_directory
+    def test_module_version(self, directory: Path):
+        "Test module version"
+        config = {}
+        metadata = {
+            'dynamic': ['version'],
+            }
+        (directory / 'tryton.cfg').write_text(dedent("""\
+                [tryton]
+                version=1.0.0
+                """))
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual('1.0.0', metadata.get('version'))
+
+    @with_temporay_directory
+    def test_dependencies(self, directory: Path):
+        "Test dependencies"
+        config = {
+            'dependencies': ['other-package > 1'],
+            }
+        metadata = {
+            'dynamic': ['dependencies'],
+            }
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual(
+            ['other-package > 1'],
+            metadata.get('dependencies', []))
+
+    @with_temporay_directory
+    def test_tryton_dependencies(self, directory: Path):
+        "Test Tryton dependencies"
+        config = {
+            'tryton_dependencies': ['other-package'],
+            }
+        metadata = {
+            'version': '1.0.0',
+            'dynamic': ['dependencies'],
+            }
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual(
+            ['other-package >= 1.0, < 1.1'],
+            metadata.get('dependencies', []))
+
+    @with_temporay_directory
+    def test_module_dependencies(self, directory: Path):
+        "Test module dependencies"
+        config = {}
+        metadata = {
+            'dynamic': ['dependencies'],
+            }
+        (directory / 'tryton.cfg').write_text(dedent("""\
+                [tryton]
+                version=1.0.0
+                depends:
+                    other_module
+                """))
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual(
+            ['trytond_other_module >= 1.0, < 1.1'],
+            metadata.get('dependencies', []))
+
+    @with_temporay_directory
+    def test_module_dependencies_prefix(self, directory: Path):
+        "Test module dependencies with prefix"
+        config = {
+            'prefixes': {'other_module': 'organisation'},
+            }
+        metadata = {
+            'dynamic': ['dependencies'],
+            }
+        (directory / 'tryton.cfg').write_text(dedent("""\
+                [tryton]
+                version=1.0.0
+                depends:
+                    other_module
+                """))
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual(
+            ['organisation_other_module >= 1.0, < 1.1'],
+            metadata.get('dependencies', []))
+
+    @with_temporay_directory
+    def test_dependencies_not_dynamic(self, directory: Path):
+        "Test dependencies not dynamic"
+        config = {
+            'dependencies': ['other_module'],
+            }
+        metadata = {
+            'dynamic': ['optional-dependencies'],
+            }
+        hook = TrytonMetadataHook(directory, config)
+        with self.assertRaisesRegex(ValueError, r"dependencies.*dynamic"):
+            hook.update(metadata)
+
+    @with_temporay_directory
+    def test_optional_dependencies(self, directory: Path):
+        "Test optional-dependencies"
+        config = {
+            'optional-dependencies': {
+                'option': ['other-package > 1'],
+                }
+            }
+        metadata = {
+            'dynamic': ['optional-dependencies'],
+            }
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual(
+            ['other-package > 1'],
+            metadata.get('optional-dependencies', {}).get('option', []))
+
+    @with_temporay_directory
+    def test_tryton_optional_dependencies(self, directory: Path):
+        "Test Tryton optional-dependencies"
+        config = {
+            'optional-dependencies': {
+                'option': ['other-package > 1'],
+                },
+            'tryton-optional-dependencies': {
+                'option': ['module'],
+                }
+            }
+        metadata = {
+            'version': '1.0.0',
+            'dynamic': ['optional-dependencies'],
+            }
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual(
+            ['other-package > 1', 'module >= 1.0, < 1.1'],
+            metadata.get('optional-dependencies', {}).get('option', []))
+
+    @with_temporay_directory
+    def test_optional_dependencies_not_dynamic(self, directory: Path):
+        "Test optional-dependencies not dynamic"
+        config = {
+            'optional-dependencies': {
+                'option': ['other_module'],
+                },
+            }
+        metadata = {
+            'dynamic': [],
+            }
+        hook = TrytonMetadataHook(directory, config)
+        with self.assertRaisesRegex(
+                ValueError, r"optional-dependencies.*dynamic"):
+            hook.update(metadata)
+
+    @with_temporay_directory
+    def test_readme(self, directory: Path):
+        "Test readme"
+        config = {
+            'readme': 'README.rst',
+            }
+        metadata = {
+            'dynamic': ['readme'],
+            }
+        (directory / 'README.rst').write_text(dedent("""\
+                Module
+                ======
+
+                description
+
+                .. toctree::
+                   :maxdepth: 2
+
+                   setup
+                   reference
+                """))
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual({
+                'content-type': 'text/x-rst',
+                'text': dedent("""\
+                        Module
+                        ======
+
+                        description
+
+                        """),
+                }, metadata.get('readme', {}))
+
+    @with_temporay_directory
+    def test_readme_markdown(self, directory: Path):
+        "Test readme"
+        config = {
+            'readme': 'README.md',
+            }
+        metadata = {
+            'dynamic': ['readme'],
+            }
+        (directory / 'README.md').write_text(dedent("""\
+                Module
+                ======
+
+                description
+                """))
+        hook = TrytonMetadataHook(directory, config)
+        hook.update(metadata)
+        self.assertEqual({
+                'content-type': 'text/markdown',
+                'text': dedent("""\
+                        Module
+                        ======
+
+                        description
+                        """),
+                }, metadata.get('readme', {}))
+
+    @with_temporay_directory
+    def test_readme_unknown_type(self, directory: Path):
+        "Test readme"
+        config = {
+            'readme': 'README.txt',
+            }
+        metadata = {
+            'dynamic': ['readme'],
+            }
+        (directory / 'README.txt').write_text('')
+        hook = TrytonMetadataHook(directory, config)
+        with self.assertRaisesRegex(ValueError, r"readme.*\.txt"):
+            hook.update(metadata)
+
+    @with_temporay_directory
+    def test_readme_not_dynamic(self, directory: Path):
+        "Test readme not dynamic"
+        config = {
+            'readme': 'README.md',
+            }
+        metadata = {
+            'dynamic': [],
+            }
+        (directory / 'README.md').write_text('')
+        hook = TrytonMetadataHook(directory, config)
+        with self.assertRaisesRegex(ValueError, r"readme.*dynamic"):
+            hook.update(metadata)
diff -r 000000000000 -r 4fdd86f378e3 hatch_tryton/tests/tools.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/hatch_tryton/tests/tools.py       Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,14 @@
+# This file is part of hatch-tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+import tempfile
+from functools import wraps
+from pathlib import Path
+
+
+def with_temporay_directory(func):
+    @wraps(func)
+    def wrapper(self, *args, **kwargs):
+        with tempfile.TemporaryDirectory() as dirname:
+            return func(self, Path(dirname), *args, **kwargs)
+    return wrapper
diff -r 000000000000 -r 4fdd86f378e3 pyproject.toml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/pyproject.toml    Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,39 @@
+[build-system]
+requires = ['hatchling >= 1']
+build-backend = 'hatchling.build'
+
+[project]
+name = 'hatch-tryton'
+dynamic = ['version']
+dependencies = ['hatchling']
+requires-python = '>=3.9'
+authors = [
+    {name = "Cédric Krier", email = "[email protected]"},
+    ]
+maintainers = [
+    {name = "Cédric Krier", email = "[email protected]"},
+    ]
+description = "A hatchling plugin for Tryton"
+readme = 'README.rst'
+license = 'MIT'
+license-files = ['LICENSE', 'COPYRIGHT']
+keywords = ["tryton", "hatch"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Environment :: Plugins",
+    "Framework :: Hatch",
+    "Framework :: Tryton",
+    "Topic :: System :: Archiving :: Packaging",
+    ]
+
+[project.entry-points.hatch]
+tryton = 'hatch_tryton.hooks'
+
+[project.urls]
+Repository = "https://code.tryton.org/hatch-tryton";
+Issues = "https://bugs.tryton.org/hatch-tryton";
+Changelog = 
"https://code.tryton.org/hatch-tryton/-/blob/branch/default/CHANGELOG";
+Forum = "https://discuss.tryton.org/tags/hatch-tryton";
+
+[tool.hatch.version]
+path = 'hatch_tryton/__init__.py'
diff -r 000000000000 -r 4fdd86f378e3 tox.ini
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tox.ini   Wed Mar 11 23:29:20 2026 +0100
@@ -0,0 +1,14 @@
+[tox]
+envlist = py39, py310, py311, py312, py313, py314, pypy3
+
+[testenv]
+usedevelop = true
+commands =
+    coverage run --omit=*/tests/*,*/.tox/* -m xmlrunner discover -s 
hatch_tryton.tests {posargs}
+commands_post =
+    coverage report
+    coverage xml
+deps =
+    coverage
+    unittest-xml-reporting
+passenv = *

Reply via email to