# https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/ # # SPDX-FileCopyrightText: © Sebastian Thomschke and contributors # SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ # [build-system] # https://backend.pdm-project.org/ requires = ["pdm-backend"] build-backend = "pdm.backend" [project] name = "kleinanzeigen-bot" dynamic = ["version"] description = "Command line tool to publish ads on kleinanzeigen.de" readme = "README.md" authors = [ {name = "sebthom", email = "sebthom@users.noreply.github.com"}, ] license = {text = "AGPL-3.0-or-later"} classifiers = [ # https://pypi.org/classifiers/ "Private :: Do Not Upload", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Operating System :: OS Independent", "Intended Audience :: End Users/Desktop", "Topic :: Office/Business", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10" ] requires-python = ">=3.10,<3.15" dependencies = [ "certifi", "colorama", "jaraco.text", # required by pkg_resources during runtime "nodriver==0.47.*", # Pin to 0.47 until upstream fixes UTF-8 decoding issues introduced in 0.48 "pydantic>=2.11.0", "ruamel.yaml", "psutil", "wcmatch", "sanitize-filename>=1.2.0", ] [dependency-groups] # https://peps.python.org/pep-0735/ dev = [ "pip-audit", "pytest>=8.3.4", "pytest-asyncio>=0.25.3", "pytest-xdist>=3.6.1", "pytest-rerunfailures", "pytest-cov>=6.0.0", "ruff", "mypy", "basedpyright", "autopep8", "yamlfix", "pyinstaller", "platformdirs", "types-requests>=2.32.0.20250515", "pytest-mock>=3.14.0", ] [project.urls] Homepage = "https://github.com/Second-Hand-Friends/kleinanzeigen-bot" Repository = "https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git" Documentation = "https://github.com/Second-Hand-Friends/kleinanzeigen-bot/README.md" Issues = "https://github.com/Second-Hand-Friends/kleinanzeigen-bot/issues" CI = "https://github.com/Second-Hand-Friends/kleinanzeigen-bot/actions" ##################### # pdm https://github.com/pdm-project/pdm/ ##################### [tool.pdm.version] # https://backend.pdm-project.org/metadata/#dynamic-project-version source = "call" getter = "version:get_version" # uses get_version() of /version.py write_to = "kleinanzeigen_bot/_version.py" write_template = "__version__ = '{}'\n" [tool.pdm.scripts] # https://pdm-project.org/latest/usage/scripts/ app = "python -m kleinanzeigen_bot" debug = "python -m pdb -m kleinanzeigen_bot" # build & packaging generate-schemas = "python scripts/generate_schemas.py" compile.cmd = "python -O -m PyInstaller pyinstaller.spec --clean --workpath .temp" compile.env = {PYTHONHASHSEED = "1", SOURCE_DATE_EPOCH = "0"} # https://pyinstaller.org/en/stable/advanced-topics.html#creating-a-reproducible-build deps = "pdm list --fields name,version,groups" "deps:tree" = "pdm list --tree" "deps:runtime" = "pdm list --fields name,version,groups --include default" "deps:runtime:tree" = "pdm list --tree --include default" # format & lint format = { composite = ["format:py", "format:yaml"] } "format:py" = { shell = "autopep8 --recursive --in-place scripts src tests --verbose && python scripts/post_autopep8.py scripts src tests" } "format:yaml" = "yamlfix scripts/ src/ tests/" lint = { composite = ["lint:ruff", "lint:mypy", "lint:pyright"] } "lint:ruff" = "ruff check --preview" "lint:mypy" = "mypy" "lint:pyright" = "basedpyright" "lint:fix" = {shell = "ruff check --preview --fix" } # tests # Run unit tests only (exclude smoke and itest) utest = "python -m pytest --capture=tee-sys -m \"not itest and not smoke\"" # Run integration tests only (exclude smoke) itest = "python -m pytest --capture=tee-sys -m \"itest and not smoke\"" # Run smoke tests only smoke = "python -m pytest --capture=tee-sys -m smoke" # Run all tests in order: unit, integration, smoke # (for CI: run these three scripts in sequence) test = { composite = ["utest", "itest", "smoke"] } # Run all tests in a single invocation for unified summary (unit tests run first) "test:unified" = "python -m pytest --capture=tee-sys" # # Coverage scripts: # - Each group writes its own data file to .temp/.coverage..xml # "coverage:prepare" = { shell = "python scripts/coverage_helper.py prepare" } "test:cov" = { composite = ["coverage:prepare", "utest:cov", "itest:cov", "smoke:cov", "coverage:combine"] } "utest:cov" = { shell = "python scripts/coverage_helper.py run .temp/.coverage-unit.sqlite .temp/coverage-unit.xml \"not itest and not smoke\"" } "itest:cov" = { shell = "python scripts/coverage_helper.py run .temp/.coverage-itest.sqlite .temp/coverage-integration.xml \"itest and not smoke\"" } "smoke:cov" = { shell = "python scripts/coverage_helper.py run .temp/.coverage-smoke.sqlite .temp/coverage-smoke.xml smoke" } "coverage:combine" = { shell = "python scripts/coverage_helper.py combine .temp/.coverage-unit.sqlite .temp/.coverage-itest.sqlite .temp/.coverage-smoke.sqlite" } # Run all tests with coverage in a single invocation "test:cov:unified" = "python -m pytest --capture=tee-sys --cov=src/kleinanzeigen_bot --cov-report=term-missing" # Test script structure: # - Composite test groups (unit, integration, smoke) are run in order to fail fast and surface critical errors early. # - This prevents running all tests if a foundational component is broken, saving time. # - Each group is covered and reported separately. # # See docs/TESTING.md for more details. ##################### # autopep8 # https://pypi.org/project/autopep8/ # https://github.com/hhatto/autopep8 ##################### [tool.autopep8] max_line_length = 160 ignore = [ # https://github.com/hhatto/autopep8#features "E124", # Don't change indention of multi-line statements "E128", # Don't change indention of multi-line statements "E231", # Don't add whitespace after colon (:) on type declaration "E251", # Don't remove whitespace around parameter '=' sign. "E401" # Don't put imports on separate lines ] aggressive = 3 ##################### # ruff # https://pypi.org/project/ruff/ # https://docs.astral.sh/ruff/configuration/ ##################### [tool.ruff] cache-dir = ".temp/cache_ruff" include = ["pyproject.toml", "scripts/**/*.py", "src/**/*.py", "tests/**/*.py"] line-length = 160 indent-width = 4 target-version = "py310" [tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ select = [ "A", # flake8-builtins "ARG", # flake8-unused-arguments "ANN", # flake8-annotations "ASYNC", # flake8-async #"BLE", # flake8-blind-except "B", # flake8-bugbear "C4", # flake8-comprehensions "COM", # flake8-commas "CPY", # flake8-copyright "DTZ", # flake8-datetimez #"EM", # flake8-errmsg #"ERA", # eradicate commented-out code "EXE", # flake8-executable "FA", # flake8-future-annotations "FBT", # flake8-boolean-trap "FIX", # flake8-fixme "G", # flake8-logging-format "ICN", # flake8-import-conventions "ISC", # flake8-implicit-str-concat "INP", # flake8-no-pep420 "INT", # flake8-gettext "LOG", # flake8-logging "PIE", # flake8-pie "PT", # flake8-pytest-style #"PTH", # flake8-use-pathlib "PYI", # flake8-pyi "Q", # flake8-quotes "RET", # flake8-return "RSE", # flake8-raise "S", # flake8-bandit "SIM", # flake8-simplify "SLF", # flake8-self "SLOT", # flake8-slots "T10", # flake8-debugger #"T20", # flake8-print "TC", # flake8-type-checking "TD", # flake8-todo "TID", # flake8-flake8-tidy-import "YTT", # flake8-2020 "E", # pycodestyle-errors "W", # pycodestyle-warnings #"C90", # mccabe "D", # pydocstyle "F", # pyflakes "FLY", # flynt "I", # isort "PERF", # perflint "PGH", # pygrep-hooks "PL", # pylint ] ignore = [ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "COM812", # Trailing comma missing "D1", # Missing docstring in ... "D200", # One-line docstring should fit on one line "D202", # No blank lines allowed after function docstring (found 1) "D203", # 1 blank line required before class docstring "D204", # 1 blank line required after class docstring "D205", # 1 blank line required between summary line and description "D209", # Multi-line docstring closing quotes should be on a separate line" "D212", # Multi-line docstring summary should start at the first line "D213", # Multi-line docstring summary should start at the second line "D400", # First line should end with a period "D401", # First line of docstring should be in imperative mood "D402", # First line should not be the function's signature "D404", # First word of the docstring should not be "This" "D413", # Missing blank line after last section ("Returns")" "D415", # First line should end with a period, question mark, or exclamation point "D417", # Missing argument description in the docstring for #"E124", # Don't change indention of multi-line statements #"E128", # Don't change indention of multi-line statements "E231", # Don't add whitespace after colon (:) on type declaration "E251", # Don't remove whitespace around parameter '=' sign. "E401", # Don't put imports on separate lines "FIX002", # Line contains TODO, consider resolving the issue "PERF203", # `try`-`except` within a loop incurs performance overhead "RET504", # Unnecessary assignment to `...` before `return` statement "PLR6301", # Method `...` could be a function, class method, or static method "PLR0913", # Too many arguments in function definition (needed to match parent signature) "PYI041", # Use `float` instead of `int | float` "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM105", # Use `contextlib.suppress(TimeoutError)` instead of `try`-`except`-`pass` "SIM114", # Combine `if` branches using logical `or` operator "TC006", # Add quotes to type expression in `typing.cast()` "TD002", # Missing author in TODO "TD003", # Missing issue link for this TODO ] [tool.ruff.lint.per-file-ignores] "scripts/**/*.py" = [ "INP001", # File `...` is part of an implicit namespace package. Add an `__init__.py`. ] "tests/**/*.py" = [ "ARG", "B", "FBT", "INP", "SLF", "S101", # Use of `assert` detected "PLR0904", # Too many public methods (12 > 10) "PLR2004", # Magic value used in comparison ] [tool.ruff.lint.flake8-copyright] notice-rgx = "SPDX-FileCopyrightText: .*" min-file-size = 256 [tool.ruff.lint.isort] # combine-straight-imports = true # not (yet) supported by ruff [tool.ruff.lint.pylint] # https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#design-checker # https://pylint.pycqa.org/en/latest/user_guide/checkers/features.html#design-checker-messages max-args = 6 # max. number of args for function / method (R0913) # max-attributes = 15 # TODO max. number of instance attrs for a class (R0902) max-branches = 45 # max. number of branch for function / method body (R0912) max-locals = 30 # max. number of local vars for function / method body (R0914) max-returns = 15 # max. number of return / yield for function / method body (R0911) max-statements = 150 # max. number of statements in function / method body (R0915) max-public-methods = 25 # max. number of public methods for a class (R0904) # max-positional-arguments = 5 # max. number of positional args for function / method (R0917) ##################### # mypy # https://github.com/python/mypy ##################### [tool.mypy] # https://mypy.readthedocs.io/en/stable/config_file.html #mypy_path = "$MYPY_CONFIG_FILE_DIR/tests/stubs" cache_dir = ".temp/cache_mypy" python_version = "3.10" files = "scripts,src,tests" strict = true disallow_untyped_calls = false disallow_untyped_defs = true disallow_incomplete_defs = true ignore_missing_imports = true show_error_codes = true warn_unused_ignores = true verbosity = 0 ##################### # basedpyright # https://github.com/detachhead/basedpyright ##################### [tool.basedpyright] # https://docs.basedpyright.com/latest/configuration/config-files/ include = ["scripts", "src", "tests"] defineConstant = { DEBUG = false } pythonVersion = "3.10" typeCheckingMode = "standard" ##################### # pytest # https://pypi.org/project/pytest/ ##################### [tool.pytest.ini_options] cache_dir = ".temp/cache_pytest" testpaths = [ "src", "tests" ] # https://docs.pytest.org/en/stable/reference.html#confval-addopts addopts = """ --strict-markers --doctest-modules --cov-report=term-missing -n auto --durations=25 --durations-min=0.5 """ markers = [ "slow: marks a test as long running", "smoke: marks a test as a high-level smoke test (critical path, no mocks)", "itest: marks a test as an integration test (i.e. a test with external dependencies)", "asyncio: mark test as async", "unit: marks a test as a unit test" ] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" filterwarnings = [ "ignore:Exception ignored in:pytest.PytestUnraisableExceptionWarning", "ignore::DeprecationWarning" ] [tool.coverage.run] # https://coverage.readthedocs.io/en/latest/config.html#run data_file = ".temp/coverage.sqlite" branch = true # track branch coverage relative_files = true [tool.coverage.report] precision = 2 show_missing = true skip_covered = false include = ["src/kleinanzeigen_bot/*"] ##################### # yamlfix # https://lyz-code.github.io/yamlfix/ ##################### [tool.yamlfix] allow_duplicate_keys = true comments_min_spaces_from_content = 2 comments_require_starting_space = false # FIXME should be true but rule is buggy comments_whitelines = 1 section_whitelines = 1 explicit_start = false indentation = 2 line_length = 1024 preserve_quotes = true quote_basic_values = false quote_keys_and_basic_values = false quote_representation = '"' whitelines = 1