diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6022cd5..a6ff77c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,24 +8,23 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: '3.10' - architecture: 'x64' + python-version: '3.x' - - name: Install dependencies and package + - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install ".[build]" - name: Build source and binary distribution package run: | - python setup.py sdist bdist_wheel - env: - PACKAGE_VERSION: ${{ github.ref }} + python -m build - name: Check distribution package run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28be853..125b268 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,47 +2,39 @@ name: Run linter and tests on: [push, pull_request] jobs: - test: + build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} - - name: Install dependencies and package - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + - name: Install dependencies and package + run: | + python -m pip install --upgrade pip + pip install -e ".[test,dev]" - - name: Check formatting with black - run: | - black --check './param_parser' + - name: Run lint and code review + run: | + pre-commit run --all-files - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 './param_parser' --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 './param_parser' --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Run tests with coverage + run: | + coverage run --source='./param_parser' -m pytest + coverage xml - - name: Run tests - run: | - # run tests with coverage - coverage run --source='./param_parser' -m pytest - coverage xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 diff --git a/.gitignore b/.gitignore index 0904884..a9cba74 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ coverage.xml *.egg-info *.pyc *.pyo +*.sqlite* +build/ +dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ff4f1b6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + args: ["--markdown-linebreak-ext=md"] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + + - repo: https://github.com/asottile/add-trailing-comma + rev: v4.0.0 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.14 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/LICENSE b/LICENSE index e3ff47d..3be78ef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2022 ANEXIA Internetdienstleistungs GmbH +Copyright (c) 2022-2026 Anexia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 982323b..8ca8837 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ param-parser [![Test Status](https://github.com/anexia/python-param-parser/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/anexia/python-param-parser/actions/workflows/test.yml) [![Codecov](https://codecov.io/gh/anexia/python-param-parser/branch/main/graph/badge.svg)](https://codecov.io/gh/anexia/python-param-parser) -`param-parser` is a parser library for a param string expression. Those expressions are arbitrary strings with +`param-parser` is a parser library for a param string expression. Those expressions are arbitrary strings with placeholders in it, where a placeholder consists of a name, an optional type and a list of options. # Installation @@ -32,8 +32,8 @@ this-is-a-{param:string}-expression this-is-a-{param}-expression ``` -As you see, a param is introduced by an opening curly bracket, followed by the name of the param, a colon, the type of -the param, another colon and a comma separated list of options. The param configuration gets terminated by a closing +As you see, a param is introduced by an opening curly bracket, followed by the name of the param, a colon, the type of +the param, another colon and a comma separated list of options. The param configuration gets terminated by a closing curly bracket. Note that the type and option configuration are optional, but the name is mandatory. To parse an expression shown above, use the Python code as follows: @@ -70,6 +70,7 @@ result[0].sequence_value # Gets `"this-is-a-{param:string:option1,option2,optio | This Project | Python Version | |--------------|----------------| +| 1.2.* | 3.10-3.14 | | 1.1.* | 3.9-3.13 | | 1.0.* | 3.7-3.11 | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5949fc2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Reporting Security Issues + +Please report any security issues you discovered to opensource[at]anexia-it[dot]com + +We will assess the risk, plus make a fix available before we create a GitHub issue. + +Thank you for your contribution. diff --git a/param_parser/exception.py b/param_parser/exception.py index 11247e0..f6927aa 100644 --- a/param_parser/exception.py +++ b/param_parser/exception.py @@ -14,7 +14,4 @@ def __init__(self, message: str): super().__init__(self._message) def __repr__(self) -> str: - return "{}({})".format( - self.__class__.__name__, - repr(self._message), - ) + return f"{self.__class__.__name__}({repr(self._message)})" diff --git a/param_parser/lexer.py b/param_parser/lexer.py index 01ba88b..2a18b29 100644 --- a/param_parser/lexer.py +++ b/param_parser/lexer.py @@ -1,7 +1,6 @@ import abc -import typing -from .token import * +from .token import Token __all__ = [ @@ -39,7 +38,7 @@ def get_next_token(self) -> Token: pass @property - def current_character(self) -> typing.Optional[str]: + def current_character(self) -> str | None: """ Gets the current character the position pointer points to on the expression. If the position pointer exceeds the length of the expression, :data:`None` gets returned. @@ -68,7 +67,10 @@ def read_single(self, token_type: str) -> Token: :return: The produced token instance. """ token = Token( - self._position, token_type, self.current_character, self.current_character + self._position, + token_type, + self.current_character, + self.current_character, ) self.advance() @@ -102,10 +104,7 @@ def read_sequence(self, token_type: str, until: list = None) -> Token: return Token(position, token_type, sequence, raw_sequence) def __repr__(self) -> str: - return "{}({})".format( - self.__class__.__name__, - repr(self._expression), - ) + return f"{self.__class__.__name__}({repr(self._expression)})" class ParamLexer(Lexer): diff --git a/param_parser/node.py b/param_parser/node.py index e95dc98..f558f0c 100644 --- a/param_parser/node.py +++ b/param_parser/node.py @@ -1,5 +1,4 @@ import abc -import typing __all__ = [ @@ -16,9 +15,7 @@ class Node(abc.ABC): """ def __repr__(self) -> str: - return "{}()".format( - self.__class__.__name__, - ) + return f"{self.__class__.__name__}()" class ParamNode(Node): @@ -30,8 +27,8 @@ class ParamNode(Node): def __init__( self, param_name: str, - param_type: typing.Optional[str], - param_options: typing.Optional[list], + param_type: str | None, + param_options: list | None, ): """ Init method for the class. @@ -41,8 +38,8 @@ def __init__( :param param_options: The list of options of the param. Becomes an empty list if passed :data:`None`. """ self._param_name: str = param_name - self._param_type: typing.Optional[str] = param_type or None - self._param_options: typing.List[str] = param_options or [] + self._param_type: str | None = param_type or None + self._param_options: list[str] = param_options or [] @property def param_name(self) -> str: @@ -54,7 +51,7 @@ def param_name(self) -> str: return self._param_name @property - def param_type(self) -> typing.Optional[str]: + def param_type(self) -> str | None: """ Gets the type of the param. @@ -63,7 +60,7 @@ def param_type(self) -> typing.Optional[str]: return self._param_type @property - def param_options(self) -> typing.List[str]: + def param_options(self) -> list[str]: """ Gets the list of options of the param. Each option is a string. @@ -72,12 +69,7 @@ def param_options(self) -> typing.List[str]: return self._param_options def __repr__(self) -> str: - return "{}({}, {}, {})".format( - self.__class__.__name__, - repr(self._param_name), - repr(self._param_type), - repr(self._param_options), - ) + return f"{self.__class__.__name__}({repr(self._param_name)}, {repr(self._param_type)}, {repr(self._param_options)})" class SequenceNode(Node): @@ -103,7 +95,4 @@ def sequence_value(self) -> str: return self._sequence_value def __repr__(self) -> str: - return "{}({})".format( - self.__class__.__name__, - repr(self._sequence_value), - ) + return f"{self.__class__.__name__}({repr(self._sequence_value)})" diff --git a/param_parser/parser.py b/param_parser/parser.py index 1b8a1e1..63281c0 100644 --- a/param_parser/parser.py +++ b/param_parser/parser.py @@ -1,10 +1,9 @@ import abc -import typing -from .exception import * -from .token import * -from .lexer import * -from .node import * +from .exception import ParserSyntaxException +from .token import Token +from .lexer import Lexer, ParamLexer, ParamOptionsLexer, ParamSequenceLexer +from .node import Node, ParamNode, SequenceNode __all__ = [ @@ -43,7 +42,7 @@ def parse(self) -> list: """ pass - def eat(self, token_types: typing.Optional[typing.List[str]]) -> Token: + def eat(self, token_types: list[str] | None) -> Token: """ Consumes the next token produced by the lexer. If a list of valid token types is given, this method will throw a :class:`.exception.ParserSyntaxException` if the produced token's type is not within the list. @@ -68,10 +67,7 @@ def raise_unexpected_token(self, token: Token): :return: """ raise ParserSyntaxException( - "Unexpected token '{}' at position '{}'".format( - token.token_type, - token.token_position + self._offset, - ) + f"Unexpected token '{token.token_type}' at position '{token.token_position + self._offset}'", ) @@ -81,7 +77,7 @@ class ParamParser(Parser): `"name:type:option1,option2,option3"` """ - def parse(self) -> typing.List[typing.Union[str, list]]: + def parse(self) -> list[str | list]: """ Parses the tokens generated by the lexer and returns a list containing the param configuration of the expression. The first element of the list is the name of the param as a string. The second element is the type @@ -123,7 +119,7 @@ class ParamOptionsParser(Parser): `"option1,option2,option3"` """ - def parse(self) -> typing.List[str]: + def parse(self) -> list[str]: """ Parses the tokens generated by the lexer and returns a list of strings, where each string is an option value. The order of the list equals the order of option values within the expression. @@ -149,7 +145,7 @@ class ParamSequenceParser(Parser): `"random-sequence{name:type:option1,option2,option3}other-random-sequence"` """ - def parse(self) -> typing.List[Node]: + def parse(self) -> list[Node]: """ Parses the tokens generated by the lexer and returns a list of nodes representing the structure of the expression. A node may be a :class:`.node.SequenceNode`, which represents a sequence of text without any @@ -161,7 +157,7 @@ def parse(self) -> typing.List[Node]: """ node_list = [] current_token = self.eat( - [Token.TYPE_EOF, Token.TYPE_SEQUENCE, Token.TYPE_LC_BRACKET] + [Token.TYPE_EOF, Token.TYPE_SEQUENCE, Token.TYPE_LC_BRACKET], ) while current_token.token_type != Token.TYPE_EOF: @@ -179,19 +175,19 @@ def parse(self) -> typing.List[Node]: param_sequence = param_sequence_parser.parse() node_list.append( - ParamNode(param_sequence[0], param_sequence[1], param_sequence[2]) + ParamNode(param_sequence[0], param_sequence[1], param_sequence[2]), ) self.eat([Token.TYPE_RC_BRACKET]) current_token = self.eat( - [Token.TYPE_EOF, Token.TYPE_SEQUENCE, Token.TYPE_LC_BRACKET] + [Token.TYPE_EOF, Token.TYPE_SEQUENCE, Token.TYPE_LC_BRACKET], ) return node_list -def parse(expression: str) -> typing.List[Node]: +def parse(expression: str) -> list[Node]: """ Parses the given expression and returns a list of :class:`.node.SequenceNode` and :class:`.node.ParamNode` instances. The expected expression looks as follows: diff --git a/param_parser/token.py b/param_parser/token.py index 571b603..f2de124 100644 --- a/param_parser/token.py +++ b/param_parser/token.py @@ -1,6 +1,3 @@ -import typing - - __all__ = [ "Token", ] @@ -23,8 +20,8 @@ def __init__( self, token_position: int, token_type: str, - token_value: typing.Optional[str], - token_raw_value: typing.Optional[str], + token_value: str | None, + token_raw_value: str | None, ): """ Init method for the class. @@ -58,7 +55,7 @@ def token_type(self) -> str: return self._token_type @property - def token_value(self) -> typing.Optional[str]: + def token_value(self) -> str | None: """ Gets the value of the token without the escape characters. `'EOF'` tokens have a :data:`None` value. @@ -67,7 +64,7 @@ def token_value(self) -> typing.Optional[str]: return self._token_value @property - def token_raw_value(self) -> typing.Optional[str]: + def token_raw_value(self) -> str | None: """ Gets the raw value of the token without the escape characters. `'EOF'` tokens have a :data:`None` value. @@ -76,10 +73,4 @@ def token_raw_value(self) -> typing.Optional[str]: return self._token_raw_value def __repr__(self) -> str: - return "{}({}, {}, {}, {})".format( - self.__class__.__name__, - repr(self._token_position), - repr(self._token_type), - repr(self._token_value), - repr(self._token_raw_value), - ) + return f"{self.__class__.__name__}({repr(self._token_position)}, {repr(self._token_type)}, {repr(self._token_value)}, {repr(self._token_raw_value)})" diff --git a/pyproject.toml b/pyproject.toml index 3181290..9cc5dc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,57 @@ [build-system] -requires = [ - "setuptools>=58", - "wheel", -] +requires = ["setuptools>=75", "wheel", "setuptools_scm[toml]>=8"] build-backend = "setuptools.build_meta" + +[project] +name = "param-parser" +dynamic = ["version"] +description = "A simple parameter parsing library for Python." +readme = "README.md" +requires-python = ">=3.10" +license = {file = "LICENSE"} +authors = [{name = "Anexia", email = "opensource@anexia-it.com"}] +urls = {Homepage = "https://github.com/anexia/python-param-parser"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +dependencies = [ +] + +[project.optional-dependencies] +test = [ + "coverage>=7.13.2", + "pytest>=8", +] +dev = [ + "pre-commit>=4.5,<4.6", +] +build = [ + "build>=1.4.0", + "twine>=6.2.0", + "wheel>=0.46.3", +] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["."] +include = ["param_parser*"] + +[tool.setuptools_scm] + +[tool.ruff.lint.per-file-ignores] +# Package __init__ files re-export the public API with `from .module import *`; +# every submodule defines __all__, so the surface is explicit even though ruff +# cannot resolve it across modules. +"param_parser/__init__.py" = ["F403", "F405"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 97fa733..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Package and package dependencies --e . - -# Development dependencies -pytest>=8.3,<8.4 -flake8>=7.1,<8.0 -coverage>=7.6,<8.0 -setuptools>=42 -wheel>=0.37 -twine>=3.4 -black>=22.3 diff --git a/setup.py b/setup.py deleted file mode 100644 index 536a25a..0000000 --- a/setup.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -from setuptools import find_packages, setup - -with open(os.path.join(os.path.dirname(__file__), 'README.md')) as fh: - readme = fh.read() - -# allow setup.py to be run from any path -os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) - -setup( - name='param-parser', - version=os.getenv('PACKAGE_VERSION', '0.0.0').replace('refs/tags/', ''), - packages=find_packages(), - include_package_data=True, - license='MIT', - description='A parser library for a param string expression.', - long_description=readme, - long_description_content_type='text/markdown', - url='https://github.com/anexia/python-param-parser', - author='Andreas Stocker', - author_email='AStocker@anexia-it.com', - install_requires=[], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - 'Topic :: Software Development', - ], -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_parser.py b/tests/test_parser.py index c590596..e1656ae 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -27,7 +27,7 @@ def test_single_param_node(): assert isinstance(result[0], param_parser.ParamNode) assert result[0].param_name == "this_is_a_test" - assert result[0].param_type == None + assert result[0].param_type is None assert result[0].param_options == [] @@ -43,7 +43,7 @@ def test_untyped_param_node_1(): assert result[0].sequence_value == "this/" assert result[1].param_name == "is" - assert result[1].param_type == None + assert result[1].param_type is None assert result[1].param_options == [] assert result[2].sequence_value == "/a/test" @@ -61,7 +61,7 @@ def test_untyped_param_node_2(): assert result[0].sequence_value == "this/" assert result[1].param_name == "is" - assert result[1].param_type == None + assert result[1].param_type is None assert result[1].param_options == [] assert result[2].sequence_value == "/a/test" @@ -161,7 +161,7 @@ def test_untyped_param_node_with_escaped_colon_1(): assert result[0].sequence_value == "this/" assert result[1].param_name == ":" - assert result[1].param_type == None + assert result[1].param_type is None assert result[1].param_options == [] assert result[2].sequence_value == "/a/test" @@ -179,7 +179,7 @@ def test_untyped_param_node_with_escaped_colon_2(): assert result[0].sequence_value == "this/" assert result[1].param_name == ":" - assert result[1].param_type == None + assert result[1].param_type is None assert result[1].param_options == [] assert result[2].sequence_value == "/a/test" @@ -289,25 +289,25 @@ def test_multiple_untyped_param_nodes(): assert isinstance(result[6], param_parser.ParamNode) assert result[0].param_name == "this" - assert result[0].param_type == None + assert result[0].param_type is None assert result[0].param_options == [] assert result[1].sequence_value == "/" assert result[2].param_name == "is" - assert result[2].param_type == None + assert result[2].param_type is None assert result[2].param_options == [] assert result[3].sequence_value == "/" assert result[4].param_name == "a" - assert result[4].param_type == None + assert result[4].param_type is None assert result[4].param_options == [] assert result[5].sequence_value == "/" assert result[6].param_name == "test" - assert result[6].param_type == None + assert result[6].param_type is None assert result[6].param_options == []