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 = *