hatch_tryton-0.1.0/.flake80000644000000000000000000000005113615410400012305 0ustar00[flake8] ignore=E123,E124,E126,E128,W503 hatch_tryton-0.1.0/.gitlab-ci.yml0000644000000000000000000000250413615410400013573 0ustar00workflow: 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 hatch_tryton-0.1.0/.hgtags0000644000000000000000000000005713615410400012416 0ustar0047af3905024693b78a68aa9a69ca82fbcf7ab27b 0.1.0 hatch_tryton-0.1.0/CHANGELOG0000644000000000000000000000010713615410400012346 0ustar00 Version 0.1.0 - 2026-03-19 -------------------------- Initial release hatch_tryton-0.1.0/tox.ini0000644000000000000000000000047013615410400012452 0ustar00[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 = * hatch_tryton-0.1.0/hatch_tryton/__init__.py0000644000000000000000000000025513615410400015757 0ustar00# 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.1.0' hatch_tryton-0.1.0/hatch_tryton/hooks.py0000644000000000000000000000046313615410400015344 0ustar00# 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 hatch_tryton-0.1.0/hatch_tryton/plugin.py0000644000000000000000000001320413615410400015514 0ustar00# 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 email.utils import parseaddr from pathlib import Path from typing import Generator, Optional from hatchling.metadata.plugin.interface import MetadataHookInterface TrytonConfig = namedtuple( 'TrytonConfig', ['version', 'depends', 'extras_depend']) Author = namedtuple('Author', ['name', 'email']) _COPYRIGHT_PREFIX = "Copyright (C) " 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(), extras_depend=config.get( 'tryton', 'extras_depend', 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: if module in {'ir', 'res', 'tests'}: return 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, } def _get_authors(root: str, path: str) -> Generator[Author, None, None]: copyright_path = Path(root) / path with copyright_path.open() as f: for line in f: line = line.strip() if not line.lower().startswith(_COPYRIGHT_PREFIX.lower()): continue try: _, name = line[len(_COPYRIGHT_PREFIX):].split(' ', 1) except ValueError: continue if '@' in name: name, email = parseaddr(name) else: email = '' yield Author(name=name, email=email) 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 dependencies.append(_set_version('trytond', series)) for module in tryton_cfg.depends: if 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_cfg and tryton_cfg.extras_depend: test_depends = optional_dependencies.setdefault('test', []) for module in tryton_cfg.extras_depend: if package := _package_name(module, prefixes): test_depends.append(_set_version(package, series)) 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) if ((copyright := self.config.get('copyright')) and 'authors' in metadata.get('dynamic', [])): metadata['authors'] = list( map(Author._asdict, _get_authors(self.root, copyright))) hatch_tryton-0.1.0/hatch_tryton/tests/__init__.py0000644000000000000000000000022613615410400017117 0ustar00# 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. hatch_tryton-0.1.0/hatch_tryton/tests/test_metadata.py0000644000000000000000000003037313615410400020205 0ustar00# 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 Author, TrytonMetadataHook, _get_authors 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', 'dependencies'], } (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 >= 1.0, < 1.1', 'trytond_other_module >= 1.0, < 1.1', ], metadata.get('dependencies', [])) @with_temporay_directory def test_base_module_dependencies(self, directory: Path): "Test base module dependencies" config = {} metadata = { 'dynamic': ['dependencies'], } (directory / 'tryton.cfg').write_text(dedent("""\ [tryton] version=1.0.0 depends: res ir tests """)) hook = TrytonMetadataHook(directory, config) hook.update(metadata) self.assertEqual( ['trytond >= 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([ 'trytond >= 1.0, < 1.1', '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_module_extra_dependencies(self, directory: Path): "Test module extra dependencies" config = {} metadata = { 'dynamic': ['dependencies', 'optional-dependencies'], } (directory / 'tryton.cfg').write_text(dedent("""\ [tryton] version=1.0.0 extras_depend: other_module """)) hook = TrytonMetadataHook(directory, config) hook.update(metadata) self.assertEqual({ 'test': ['trytond_other_module >= 1.0, < 1.1'], }, metadata.get('optional-dependencies', {})) @with_temporay_directory def test_module_extra__tryton_optional_dependencies(self, directory: Path): "Test module extra and Tryton optional dependencies" config = { 'tryton-optional-dependencies': { 'test': ['module'], }, } metadata = { 'dynamic': ['dependencies', 'optional-dependencies'], } (directory / 'tryton.cfg').write_text(dedent("""\ [tryton] version=1.0.0 extras_depend: other_module """)) hook = TrytonMetadataHook(directory, config) hook.update(metadata) self.assertEqual({ 'test': [ 'trytond_other_module >= 1.0, < 1.1', 'module >= 1.0, < 1.1', ], }, metadata.get('optional-dependencies', {})) @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) @with_temporay_directory def test_authors_from_copyright(self, directory: Path): "Test authors from copyright file" config = { 'copyright': 'COPYRIGHT', } metadata = { 'dynamic': ['authors'], } (directory / 'COPYRIGHT').write_text(dedent("""\ Copyright (C) 9999 John Doe Copyright (C) Foo Text""")) hook = TrytonMetadataHook(directory, config) hook.update(metadata) self.assertEqual( [{'name': "John Doe", 'email': ''}], metadata.get('authors')) @with_temporay_directory def test_authors_from_copyright_not_dynamic(self, directory: Path): "Test authors from copyright file not dynamic" config = { 'copyright': 'COPYRIGHT', } metadata = { 'authors': [{'name': "Jane Doe", 'email': ''}], } (directory / 'COPYRIGHT').write_text(dedent("""\ Copyright (C) 9999 John Doe Text""")) hook = TrytonMetadataHook(directory, config) hook.update(metadata) self.assertEqual( [{'name': "Jane Doe", 'email': ''}], metadata.get('authors')) @with_temporay_directory def test_get_authors(self, directory: Path): "Test get authors from copyright file" (directory / 'COPYRIGHT').write_text(dedent("""\ Copyright (C) 9999 John Doe Copyright (C) 0000-9999 Jane Doe Copyright (C) 9999 John Doe Copyright (C) 9999 junk""")) authors = list(_get_authors(directory, 'COPYRIGHT')) self.assertEqual([ Author("John Doe", ''), Author("Jane Doe", ''), Author("John Doe", 'john.doe@example.com'), Author('', 'email@example.com'), ], authors) hatch_tryton-0.1.0/hatch_tryton/tests/tools.py0000644000000000000000000000070213615410400016517 0ustar00# 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 hatch_tryton-0.1.0/.gitignore0000755000000000000000000000000013615410400021125 2Sync/dotfiles/git/dot-gitignoreustar00hatch_tryton-0.1.0/.hgignore0000644000000000000000000000007713615410400012745 0ustar00syntax: glob *.py[cdo] *.egg-info dist/ build/ .tox/ .coverage hatch_tryton-0.1.0/COPYRIGHT0000644000000000000000000000012513615410400012427 0ustar00Copyright (C) 2026 Cédric Krier Copyright (C) 2026 B2CK SRL hatch_tryton-0.1.0/LICENSE0000644000000000000000000000201413615410400012140 0ustar00MIT 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. hatch_tryton-0.1.0/README.rst0000644000000000000000000000274413615410400012634 0ustar00============ hatch-tryton ============ A ``hatchling`` plugin to manage Tryton dependencies. Usage ===== #. Include it as a plugin to your ``pyproject.toml``: .. code:: toml [build-system] requires = ['hatchling', 'hatch-tryton'] build-backend = 'hatchling.build' #. Mark your ``version``, ``dependencies``, ``optional-dependencies``, ``readme`` and ``authors`` fields as ``dynamic``: .. code:: toml [project] dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme'] #. And configure the plugin: .. code:: toml [tool.hatch.metadata.hooks.tryton] # Path to the ``tryton.cfg`` file config = 'tryton.cfg' # Dictionary mapping module names to their package prefixes prefixes = {} # List of dependencies that are not Tryton modules dependencies = [] # List of dependencies that are specifically Tryton modules tryton_dependencies = [] # Path to the README file readme = '' # Path to the copyright file copyright = '' [tool.hatch.metadata.hooks.tryton.optional-dependencies] # Add any optional non-Tryton dependencies here [tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies] # Add any optional Tryton dependencies here .. note:: The ``depends`` in ``tryton.cfg`` are added automatically as dependencies. The ``extras_depend`` in ``tryton.cfg`` are added automatically as ``test`` option dependencies. hatch_tryton-0.1.0/pyproject.toml0000644000000000000000000000213413615410400014052 0ustar00[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 = "cedric.krier@b2ck.com"}, ] maintainers = [ {name = "Tryton", email = "foundation@tryton.org"}, ] 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] homepage = "https://www.tryton.org/" changelog = "https://code.tryton.org/hatch-tryton/-/blob/branch/default/CHANGELOG" forum = "https://discuss.tryton.org/tags/hatch-tryton" issues = "https://bugs.tryton.org/hatch-tryton" repository = "https://code.tryton.org/hatch-tryton" [tool.hatch.version] path = 'hatch_tryton/__init__.py' hatch_tryton-0.1.0/PKG-INFO0000644000000000000000000000456013615410400012240 0ustar00Metadata-Version: 2.4 Name: hatch-tryton Version: 0.1.0 Summary: A hatchling plugin for Tryton Project-URL: homepage, https://www.tryton.org/ Project-URL: changelog, https://code.tryton.org/hatch-tryton/-/blob/branch/default/CHANGELOG Project-URL: forum, https://discuss.tryton.org/tags/hatch-tryton Project-URL: issues, https://bugs.tryton.org/hatch-tryton Project-URL: repository, https://code.tryton.org/hatch-tryton Author-email: Cédric Krier Maintainer-email: Tryton License-Expression: MIT License-File: COPYRIGHT License-File: LICENSE Keywords: hatch,tryton Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Plugins Classifier: Framework :: Hatch Classifier: Framework :: Tryton Classifier: Topic :: System :: Archiving :: Packaging Requires-Python: >=3.9 Requires-Dist: hatchling Description-Content-Type: text/x-rst ============ hatch-tryton ============ A ``hatchling`` plugin to manage Tryton dependencies. Usage ===== #. Include it as a plugin to your ``pyproject.toml``: .. code:: toml [build-system] requires = ['hatchling', 'hatch-tryton'] build-backend = 'hatchling.build' #. Mark your ``version``, ``dependencies``, ``optional-dependencies``, ``readme`` and ``authors`` fields as ``dynamic``: .. code:: toml [project] dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme'] #. And configure the plugin: .. code:: toml [tool.hatch.metadata.hooks.tryton] # Path to the ``tryton.cfg`` file config = 'tryton.cfg' # Dictionary mapping module names to their package prefixes prefixes = {} # List of dependencies that are not Tryton modules dependencies = [] # List of dependencies that are specifically Tryton modules tryton_dependencies = [] # Path to the README file readme = '' # Path to the copyright file copyright = '' [tool.hatch.metadata.hooks.tryton.optional-dependencies] # Add any optional non-Tryton dependencies here [tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies] # Add any optional Tryton dependencies here .. note:: The ``depends`` in ``tryton.cfg`` are added automatically as dependencies. The ``extras_depend`` in ``tryton.cfg`` are added automatically as ``test`` option dependencies.