[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "derivepassphrase"
description = "An almost faithful Python reimplementation of James Coglan's vault."
readme = "README.md"
requires-python = ">= 3.9"
license = { text = "zlib/libpng" }
keywords = []
authors = [
  { name = "Marco Ricci", email = "software@the13thletter.info" },
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Environment :: Console",
  "License :: OSI Approved :: zlib/libpng License",
  "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",
  "Programming Language :: Python :: Implementation :: CPython",
  "Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
  # We use click for the command-line interface.  We require version 8.1.0
  # or higher due to click issue #1985.
  "click >= 8.1",
  # We include type annotations, and use facilities that are not readily
  # available in older Pythons (such as typing.Self).  These are loaded from
  # typing_extensions, instead of using explicit version guards.
  "typing_extensions",
  # We read configuration files in JSON and TOML format.  The latter is
  # unavailable in the Python standard library until Python 3.11.
  'tomli; python_version < "3.11"'
]
dynamic = ['version']

[project.optional-dependencies]
dev = [
  # Development uses the hatch build system, to isolate all tools in their
  # own virtual environment.
  "hatch ~= 1.10",
]
export = [
  # The vault configuration exporter relies on cryptography.  Version 38 was
  # the first to include the `algorithms.AES256` interface, instead of only
  # the `algorithms.AES` interface.
  "cryptography >= 38.0.0",
]

[project.scripts]
derivepassphrase = "derivepassphrase.cli:derivepassphrase"

[project.urls]
Documentation = "https://the13thletter.info/derivepassphrase/"
Issues = "https://github.com/the-13th-letter/derivepassphrase/issues"
Source = "https://github.com/the-13th-letter/derivepassphrase"

[tool.coverage.html]
directory = "html/coverage"

[tool.coverage.paths]
src = ["src"]
tests = ["tests"]

[tool.coverage.report]
skip_covered = false
skip_empty = true
precision = 3
partial_branches = [
    'pragma: no branch',
]
exclude_also = [
  "if __name__ == .__main__.:",
  'if (?:typing\.)?TYPE_CHECKING:',
  "raise AssertionError",
  "raise NotImplementedError",
  'assert False',
  '(?:typing\.)?assert_never\(',
  '@overload',
  'class .*\(Protocol\):',
]

[tool.coverage.run]
source_pkgs = ["derivepassphrase", "tests"]
branch = true
parallel = true
omit = [
  "__main__.py",
]
dynamic_context = 'test_function'

[tool.hatch.build.targets.sdist]
exclude = [
    'docs/changelog.d/*.md',
]

[tool.hatch.build.targets.wheel]
packages = ['src/derivepassphrase']

[tool.hatch.env]
requires = [
  "hatch-mkdocs",
]

[tool.hatch.env.collectors.mkdocs.docs]
path = "mkdocs.yml"

[tool.hatch.envs.docs]
extra-dependencies = [
  # Our documentation uses formatted function signatures (i.e. with
  # formatted type annotations), which requires `black`.
  "black",
  # Our changelog is assembled from singular entries, orchestrated by
  # `scriv`.
  "scriv >= 1.4"
]
detached = false

[tool.hatch.envs.hatch-static-analysis]
config-path = "/dev/null"
dependencies = [
  "ruff ~= 0.8.0",
]

[tool.hatch.envs.hatch-test]
default-args = ['src', 'tests']
extra-dependencies = [
    "hypothesis >= 6.0",
]
matrix-name-format = '{variable}_{value}'

[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.13", "3.12", "3.11", "3.10", "3.9", "pypy3.10", "pypy3.9"]
cryptography = ["no", "yes"]
hypothesis-profile = ["user-default"]
parser-version = ["PEG"]

[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.9", "pypy3.9"]
cryptography = ["no", "yes"]
hypothesis-profile = ["user-default"]
parser-version = ["LL1"]

[[tool.hatch.envs.hatch-test.matrix]]
cryptography = ["yes"]
hypothesis-profile = ["ci"]

[tool.hatch.envs.hatch-test.overrides]
matrix.cryptography.features = [
    { value = "export", if = ["yes"] },
]
matrix.hypothesis-profile.env-vars = [
    { key = "HYPOTHESIS_PROFILE", if = ["ci", "default", "dev", "debug"] },
]
matrix.parser-version.env-vars = [
    { key = "PYTHONOLDPARSER", value = "1", if = ["LL1"] },
]

[tool.hatch.envs.hatch-test.scripts]
run = "pytest --hypothesis-profile={env:HYPOTHESIS_PROFILE:default}{env:HATCH_TEST_ARGS:} {args}"
run-cov = "coverage run -m pytest --hypothesis-profile={env:HYPOTHESIS_PROFILE:default}{env:HATCH_TEST_ARGS:} {args}"
cov-combine = "coverage combine"
cov-report = "coverage report"

[tool.hatch.envs.types]
extra-dependencies = [
  "hypothesis >= 6.0",
  "mypy ~= 1.0",
  "pytest ~= 8.1",
]
features = [
    "export",
]

[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/derivepassphrase tests}"

[tool.hatch.version]
path = "src/derivepassphrase/__init__.py"

[tool.mypy]
files = ['src/**/*.py', 'tests/**/*.py']
mypy_path = '$MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/other-stubs'
explicit_package_bases = true
implicit_reexport = false
sqlite_cache = true
enable_error_code = ['ignore-without-code']

[tool.pytest.ini_options]
addopts = '--doctest-modules'
pythonpath = ['src']
testpaths = ['src', 'tests']
xfail_strict = true

[tool.ruff]
line-length = 79
src = ["src"]

[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = "dynamic"
preview = true
quote-style = 'single'

[tool.ruff.lint]
ignore = [
    # Suggested ignore by ruff when also using ruff to format.  We *do*
    # check for E501, because this usually only happens when there is
    # a text string that should be manually broken.
    'W191', 'E111', 'E114', 'E117', 'D206', 'D300', 'Q000', 'Q001',
    'Q002', 'Q003', 'COM812', 'COM819', 'ISC001', 'ISC002',
    # We use `assert` regularly to appease the type checker, and because
    # it is the right language tool for this job.
    'S101',
    # The formatter takes care of trailing commas and docstring code
    # automatically.
    'COM812', 'W505',
    # We document transitive exceptions as well (if we feel they would
    # be surprising to the user otherwise).
    'DOC502',
    # We currently don't have issues for every TODO.  Forcing an issue
    # also goes against the philosophy of TODOs as low-overhead markers
    # for future work; see
    # https://gist.github.com/dmnd/ed5d8ef8de2e4cfea174bd5dafcda382 .
    'TD003',
    # We somewhat regularly use loops where each iteration needs
    # a separate try-except block.
    'PERF203',
    # We do not currently use pathlib.  The PTH rules are unselected,
    # but FURB includes several pathlib-related rules.
    'FURB101', 'FURB103',
    # We catch type-ignore comments without specific code via the mypy
    # configuration, not via ruff.
    'PGH003',
]
preview = true
# We select here in the order of presentation on the ruff documentation
# website.  ruff default selection (v0.6.2) is merely E4, E7, E9 and F.
select = [
    'F', 'E', 'W', 'C90', 'I', 'N', 'D', 'UP', 'YTT',
    'ANN', 'ASYNC', 'S', 'BLE', 'FBT', 'B', 'A', 'COM',
    'CPY', 'C4', 'DTZ', 'T10', 'DJ', 'EM', 'EXE', 'FA',
    'ISC', 'ICN', 'LOG', 'G', 'INP', 'PIE', 'T20', 'PYI',
    'PT', 'Q', 'RET', 'SLF', 'SLOT', 'SIM', 'TID', 'TC',
    'INT', 'ARG',
    # We currently do not use pathlib. Disable 'PTH'.
    'TD',
    # We use TODOs and FIXMEs as notes for later, and don't want the
    # linter to nag about every occurrence.  Disable 'FIX'.
    #
    # The "eradicate" rule is prone to a lot of false positives, and it
    # is unclear to me, and probably confusing to read, where to apply
    # a noqa marker.  Instead, disable 'ERA', and if necessary, specify
    # it on the command-line.
    'PD', 'PGH', 'PL', 'TRY', 'FLY', 'NPY', 'FAST',
    'AIR', 'PERF', 'FURB', 'DOC', 'RUF',
]

[tool.ruff.lint.per-file-ignores]
"**/scripts/*" = [
  # Suggested by hatch.
  'INP',
  # Suggested by hatch.
  'T20',
]
"**/tests/**/*" = [
  # Suggested by hatch, assumingly because it may be important to verify
  # that the value is exactly the empty string, and not just any falsy
  # value.
  'PLC1901',
  # Suggested by hatch, assumingly because tests may use "magic values".
  'PLR2004',
  # Suggested by hatch, because tests are typically organized as classes
  # and instance methods but may not really be using the `self`
  # argument.
  'PLR6301',
  # Suggested by hatch, because these warnings may be precisely what the
  # tests are supposed to test.
  'S',
  # Suggested by hatch, because pytest-style tests conventionally import
  # code from each other via relative imports.
  'TID252',
  # Our tests regularly use arguments named `input` to store an input
  # (text-/byte-)string.
  'A002',
  # We regularly annotate pytest fixtures like monkeypatch as `Any`.
  'ANN401',
  # Our tests generally don't contain docstrings.
  'D', 'DOC',
  # Our tests are regularly parametrized with booleans, for benign
  # purposes.
  'FBT',
  # One of our standard modules is called `derivepassphrase._types`.
  # Importing this from the tests directory would then automatically
  # trigger `PLC2701`.
  'PLC2701',
  # Too many public methods/arguments/returns/branches/locals doesn't
  # really apply here.
  'PLR0904', 'PLR0911', 'PLR0912', 'PLR0913', 'PLR0914', 'PLR0915',
  'PLR0916', 'PLR0917',
  # To fully test the `derivepassphrase.cli` module (and a couple other
  # things), we need to call and to mock several internal functions,
  # which would automatically trigger `SLF001`.
  'SLF001',
  # pytest does not support sensible introspection of `assert all(...)`
  # expressions in tests the same way it supports introspection in
  # `asssert all([...])`.  So the extra list comprehension actually
  # improves debuggability in this case.
  'C419',
  # The tests sometimes include long strings (in non-Python formats)
  # that should be included verbatim, without artificial line breaking,
  # so they can be grepped for.
  'E501',
]

[tool.ruff.lint.flake8-copyright]
# Include hatch-enforced SPDX-FileCopyrightText in check.
notice-rgx = '(?i)(?:Copyright\s+((?:\(C\)|©)\s+)?|SPDX-FileCopyrightText:\s+)\d{4}((-|,\s)\d{4})*'

[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
parametrize-names-type = 'list'

[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"

[tool.ruff.lint.isort]
known-first-party = ["derivepassphrase"]

[tool.ruff.lint.pycodestyle]
ignore-overlong-task-comments = true  # for E501
max-doc-length = 72  # for W505

[tool.ruff.lint.pydocstyle]
convention = 'google'

[tool.scriv]
version = "command: hatch version"
format = "md"
fragment_directory = "docs/changelog.d"
output_file = "docs/changelog.md"
insert_marker = "<!-- scriv changelog start -->"
end_marker = "<!-- scriv changelog end -->"
md_header_level = "2"
entry_title_template = "{% if version %}{{ version }} ({% endif %}{{ date.strftime('%Y-%m-%d') }}{% if version %}){% endif %}"