From 6a488290ab540592fd2c9ad8ff605166dc0dbe3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jan 2025 10:01:17 -1000 Subject: [PATCH] Initial version --- .codecov.yml | 44 ++ .coveragerc | 35 ++ .github/FUNDING.yml | 7 + .github/ISSUE_TEMPLATE/bug_report.yml | 134 ++++++ .github/ISSUE_TEMPLATE/config.yml | 37 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 62 +++ .github/dependabot.yml | 23 + .github/workflows/auto-merge.yml | 23 + .github/workflows/ci-cd.yml | 132 ++++++ .github/workflows/codeql.yml | 44 ++ .gitignore | 106 +---- .mypy.ini | 42 ++ .pre-commit-config.yaml | 138 ++++++ .readthedocs.yaml | 25 ++ .yamllint | 18 + CHANGES.rst | 15 + CHANGES/.TEMPLATE.rst | 90 ++++ CHANGES/.gitignore | 28 ++ CHANGES/README.rst | 109 +++++ LICENSE | 1 + MANIFEST.in | 21 + Makefile | 45 ++ README.md | 2 +- README.rst | 90 ++++ docs/Makefile | 230 ++++++++++ docs/_static/README.txt | 0 docs/api.rst | 36 ++ docs/changes.rst | 18 + docs/conf.py | 465 +++++++++++++++++++++ docs/contributing/guidelines.rst | 28 ++ docs/contributing/release_guide.rst | 105 +++++ docs/index.rst | 74 ++++ docs/make.bat | 281 +++++++++++++ docs/spelling_wordlist.txt | 52 +++ examples/run.py | 14 + pyproject.toml | 36 ++ pytest.ini | 84 ++++ requirements/dev.txt | 2 + requirements/doc-spelling.txt | 2 + requirements/doc.txt | 4 + requirements/lint.txt | 1 + requirements/test.txt | 4 + requirements/towncrier.txt | 1 + src/aiohttp_asyncmdnsresolver/__init__.py | 4 + src/aiohttp_asyncmdnsresolver/_impl.py | 121 ++++++ src/aiohttp_asyncmdnsresolver/api.py | 5 + src/aiohttp_asyncmdnsresolver/py.typed | 1 + tests/conftest.py | 7 + tests/test_api.py | 9 + tests/test_impl.py | 214 ++++++++++ towncrier.toml | 68 +++ 51 files changed, 3051 insertions(+), 86 deletions(-) create mode 100644 .codecov.yml create mode 100644 .coveragerc create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/ci-cd.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .mypy.ini create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 .yamllint create mode 100644 CHANGES.rst create mode 100644 CHANGES/.TEMPLATE.rst create mode 100644 CHANGES/.gitignore create mode 100644 CHANGES/README.rst create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.rst create mode 100644 docs/Makefile create mode 100644 docs/_static/README.txt create mode 100644 docs/api.rst create mode 100644 docs/changes.rst create mode 100644 docs/conf.py create mode 100644 docs/contributing/guidelines.rst create mode 100644 docs/contributing/release_guide.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/spelling_wordlist.txt create mode 100644 examples/run.py create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 requirements/dev.txt create mode 100644 requirements/doc-spelling.txt create mode 100644 requirements/doc.txt create mode 100644 requirements/lint.txt create mode 100644 requirements/test.txt create mode 100644 requirements/towncrier.txt create mode 100644 src/aiohttp_asyncmdnsresolver/__init__.py create mode 100644 src/aiohttp_asyncmdnsresolver/_impl.py create mode 100644 src/aiohttp_asyncmdnsresolver/api.py create mode 100644 src/aiohttp_asyncmdnsresolver/py.typed create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 tests/test_impl.py create mode 100644 towncrier.toml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..d4d721e --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,44 @@ +--- + +codecov: + notify: + after_n_builds: 5 # The number of test matrix+lint jobs uploading coverage + wait_for_ci: false + + require_ci_to_pass: false + + token: >- # notsecret # repo-scoped, upload-only, stability in fork PRs + 1377c679-8317-456e-a685-644c1de1e083 + +comment: + require_changes: true + +coverage: + range: 99.34..100 + status: + patch: + default: + target: 100% + flags: + - pytest + project: + default: + target: 100% + lib: + flags: + - pytest + paths: + - aiohttp_asyncmdnsresolver/ + target: 100% + tests: + flags: + - pytest + paths: + - tests/ + target: 98.2% # 100% + typing: + flags: + - MyPy + target: 100% # 100% + +... diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..b261ca8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,35 @@ +[html] +show_contexts = true +skip_covered = false + +[paths] +_site-packages-to-src-mapping = + src + */src + *\src + */lib/pypy*/site-packages + */lib/python*/site-packages + *\Lib\site-packages + +[report] +exclude_also = + ^\s*@pytest\.mark\.xfail +# small library, don't fail when running without C-extension +fail_under = 50.00 +skip_covered = true +skip_empty = true +show_missing = true + +[run] +branch = true +cover_pylib = false +# https://coverage.rtfd.io/en/latest/contexts.html#dynamic-contexts +# dynamic_context = test_function # conflicts with `pytest-cov` if set here +parallel = true +plugins = + covdefaults +relative_files = true +source = + . +source_pkgs = + aiohttp_asyncmdnsresolver diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..09c21c0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,7 @@ +--- +# These are supported funding model platforms + +github: +- asvetlov +- webknjaz +- Dreamsorcerer diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..59e3851 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,134 @@ +--- +name: 🐞 Bug Report +description: Create a report to help us improve. +labels: +- bug +body: +- type: markdown + attributes: + value: | + **Thanks for taking a minute to file a bug report!** + + ⚠ + Verify first that your issue is not [already reported on + GitHub][issue search]. + + _Please fill out the form below with as many precise + details as possible._ + + [issue search]: ../search?q=is%3Aissue&type=issues + +- type: checkboxes + id: terms + attributes: + label: Please confirm the following + description: | + Read the [aio-libs Code of Conduct][CoC] first. Check the existing issues + on the tracker. Take into account the possibility of your report + surfacing a security vulnerability. + + [CoC]: ../../.github/blob/main/CODE_OF_CONDUCT.md + options: + - label: | + I agree to follow the [aio-libs Code of Conduct][CoC] + + [CoC]: ../../.github/blob/main/CODE_OF_CONDUCT.md + required: true + - label: | + I have checked the [current issues][issue search] for duplicates. + + [issue search]: ../search?q=is%3Aissue&type=issues + required: true + - label: >- + I understand this is open source software provided for free and + that I might not receive a timely response. + required: true + - label: | + I am positive I am **NOT** reporting a (potential) security + vulnerability, to the best of my knowledge. *(These must be shared by + submitting [this report form][vulnerability report form] instead, if + any hesitation exists.)* + + [vulnerability report form]: ../security/advisories/new + required: true + - label: >- + I am willing to submit a pull request with reporoducers as xfailing test + cases or even entire fix. *(Assign this issue to me.)* + required: false + +- type: textarea + attributes: + label: Describe the bug + description: >- + A clear and concise description of what the bug is. + validations: + required: true + +- type: textarea + attributes: + label: To Reproduce + description: >- + Describe the steps to reproduce this bug. + placeholder: | + 1. Have certain environment + 2. Run given code snippet in a certain way + 3. See some behavior described + validations: + required: true + +- type: textarea + attributes: + label: Expected behavior + description: >- + A clear and concise description of what you expected to happen. + validations: + required: true + +- type: textarea + attributes: + label: Logs/tracebacks + description: | + If applicable, add logs/tracebacks to help explain your problem. + Paste the output of the steps above, including the commands + themselves and their output/traceback etc. + render: python-traceback + validations: + required: true + +- type: textarea + attributes: + label: Python Version + description: Attach your version of Python. + render: console + value: | + $ python --version + validations: + required: true +- type: textarea + attributes: + label: aiohttp_asyncmdnsresolver Version + description: Attach your version of aiohttp_asyncmdnsresolver. + render: console + value: | + $ python -m pip show aiohttp_asyncmdnsresolver + validations: + required: true + +- type: textarea + attributes: + label: OS + placeholder: >- + For example, Arch Linux, Windows, macOS, etc. + validations: + required: true + +- type: textarea + attributes: + label: Additional context + description: | + Add any other context about the problem here. + + Describe the environment you have that lead to your issue. + This includes proxy server and other bits that are related to your case. + +... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..095db31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,37 @@ +--- + +# yamllint disable rule:line-length +# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser +# yamllint enable rule:line-length +blank_issues_enabled: false # default: true +contact_links: +- name: 🔐 Security bug report 🔥 + url: https://github.com/aio-libs/.github/security/policy + about: | + Please learn how to report security vulnerabilities here. + + For all security related bugs, send an email + instead of using this issue tracker and you + will receive a prompt response. + + For more information, see + https://github.com/aio-libs/.github/security/policy +- name: >- + [🎉 NEW 🎉] + 🤷💻🤦 GitHub Discussions + url: https://github.com/aio-libs/aiohttp_asyncmdnsresolver/discussions + about: >- + Please ask typical Q&A in the Discussions tab or on StackOverflow +- name: 🤷💻🤦 StackOverflow + url: https://stackoverflow.com/questions/tagged/aiohttp + about: >- + Please ask typical Q&A here or in the + Discussions tab @ https://github.com/aio-libs/aiohttp_asyncmdnsresolver/discussions +- name: 💬 Gitter Chat + url: https://gitter.im/aio-libs/Lobby + about: Chat with devs and community +- name: 📝 Code of Conduct + url: https://github.com/aio-libs/.github/blob/main/CODE_OF_CONDUCT.md + about: ❤ Be nice to other members of the community. ☮ Behave. + +... diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..45d3b73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,62 @@ +--- +name: 🚀 Feature request +description: Suggest an idea for this project. +labels: +- enhancement +body: +- type: markdown + attributes: + value: | + **Thanks for taking a minute to file a feature for aiohttp_asyncmdnsresolver!** + + ⚠ + Verify first that your feature request is not [already reported on + GitHub][issue search]. + + _Please fill out the form below with as many precise + details as possible._ + + [issue search]: ../search?q=is%3Aissue&type=issues + +- type: textarea + attributes: + label: Is your feature request related to a problem? + description: >- + Please add a clear and concise description of what + the problem is. _Ex. I'm always frustrated when [...]_ + +- type: textarea + attributes: + label: Describe the solution you'd like + description: >- + A clear and concise description of what you want to happen. + validations: + required: true + +- type: textarea + attributes: + label: Describe alternatives you've considered + description: >- + A clear and concise description of any alternative solutions + or features you've considered. + validations: + required: true + +- type: textarea + attributes: + label: Additional context + description: >- + Add any other context or screenshots about + the feature request here. + +- type: checkboxes + attributes: + label: Code of Conduct + description: | + Read the [aio-libs Code of Conduct][CoC] first. + + [CoC]: https://github.com/aio-libs/.github/blob/main/CODE_OF_CONDUCT.md + options: + - label: I agree to follow the aio-libs Code of Conduct + required: true +... diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..493e5f0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +--- + +version: 2 +updates: + +# Maintain dependencies for GitHub Actions +- package-ecosystem: "github-actions" + directory: "/" + labels: + - dependencies + schedule: + interval: "daily" + +# Maintain dependencies for Python +- package-ecosystem: "pip" + directory: "/" + labels: + - dependencies + schedule: + interval: "daily" + open-pull-requests-limit: 10 + +... diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..9b44077 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,23 @@ +name: Dependabot auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + timeout-minutes: 20 + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2.2.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..5a40733 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,132 @@ +--- +name: CI + +on: + push: + branches: + - main + - '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6 + tags: [ 'v*' ] + pull_request: + branches: + - main + - '[0-9].[0-9]+' + + +jobs: + lint: + name: Linter + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + cache: 'pip' + cache-dependency-path: 'requirements/*.txt' + - name: Install dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: requirements/dev.txt + - name: Install itself + run: | + pip install . + - name: Prepare twine checker + run: | + pip install -U build twine wheel + python -m build + - name: Run twine checker + run: | + twine check dist/* + + test: + name: Test + strategy: + matrix: + pyver: ['3.9', '3.10', '3.11', '3.12', '3.13'] + os: [ubuntu, macos, windows] + include: + - pyver: pypy-3.10 + os: ubuntu + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.pyver }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.pyver }} + cache: 'pip' + cache-dependency-path: 'requirements/*.txt' + - name: Install dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: requirements/dev.txt + - name: Install itself + run: | + pip install . + - name: Run unittests + run: python -m pytest + env: + COLOR: 'yes' + - run: python -m coverage xml + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: [lint, test] + timeout-minutes: 20 + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + deploy: + name: Deploy + environment: pypi + timeout-minutes: 20 + runs-on: ubuntu-latest + needs: [check] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + permissions: + contents: write # GitHub Releases + id-token: write # Trusted publishing & sigstore + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install dependencies + run: + python -m pip install -U pip wheel setuptools build twine + - name: Build dists + run: | + python -m build + - name: Make Release + uses: aio-libs/create-release@v1.6.6 + with: + changes_file: CHANGES.rst + name: aiohttp-asyncmdnsresolver + version_file: aiohttp_asyncmdnsresolver/__init__.py + github_token: ${{ secrets.GITHUB_TOKEN }} + dist_dir: dist + fix_issue_regex: "`#(\\d+) `" + fix_issue_repl: "(#\\1)" + - name: >- + Publish 🐍📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..ac175a3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +--- +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "33 2 * * 2" + +jobs: + analyze: + timeout-minutes: 20 + + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.gitignore b/.gitignore index 15201ac..9409d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python +env/ build/ develop-eggs/ dist/ @@ -19,12 +20,9 @@ lib64/ parts/ sdist/ var/ -wheels/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -39,17 +37,13 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*.cover -*.py,cover +*,cover .hypothesis/ -.pytest_cache/ -cover/ # Translations *.mo @@ -58,8 +52,6 @@ cover/ # Django stuff: *.log local_settings.py -db.sqlite3 -db.sqlite3-journal # Flask stuff: instance/ @@ -72,100 +64,44 @@ instance/ docs/_build/ # PyBuilder -.pybuilder/ target/ -# Jupyter Notebook +# IPython Notebook .ipynb_checkpoints -# IPython -profile_default/ -ipython_config.py - # pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid +.python-version -# SageMath parsed files -*.sage.py +# celery beat schedule file +celerybeat-schedule -# Environments +# dotenv .env -.venv -env/ + +# virtualenv +.venv/ venv/ ENV/ -env.bak/ -venv.bak/ # Spyder project settings .spyderproject -.spyproject # Rope project settings .ropeproject -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +coverage -# Pyre type checker -.pyre/ -# pytype static type analyzer -.pytype/ +aiohttp_asyncmdnsresolver/*.c +aiohttp_asyncmdnsresolver/*.html -# Cython debug symbols -cython_debug/ +.develop -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Idea +.idea -# PyPI configuration file -.pypirc +.mypy_cache +.install-cython +.install-deps +.pytest_cache +pip-wheel-metadata diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..460e058 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,42 @@ +[mypy] +python_version = 3.9 +color_output = true +error_summary = true +files = + tests/, + src/aiohttp_asyncmdnsresolver/ + +# check_untyped_defs = true + +# disallow_untyped_calls = true +# disallow_untyped_defs = true +# disallow_any_generics = true + +enable_error_code = + ignore-without-code + +follow_imports = normal + +ignore_missing_imports = false + +pretty = true + +show_column_numbers = true +show_error_codes = true +strict_optional = true + +warn_no_return = true +warn_redundant_casts = true +warn_unused_ignores = true + +[mypy-distutils.*] +ignore_missing_imports = true + +[mypy-expandvars] +ignore_missing_imports = true + +[mypy-pytest] +ignore_missing_imports = true + +[mypy-tomllib] +ignore_missing_imports = true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4be59f3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,138 @@ +--- + +ci: + autoupdate_schedule: quarterly + skip: + - actionlint-docker + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v5.0.0' + hooks: + - id: check-merge-conflict +- repo: https://github.com/asottile/yesqa + rev: v1.5.0 + hooks: + - id: yesqa + additional_dependencies: + - wemake-python-styleguide + +- repo: https://github.com/python-jsonschema/check-jsonschema.git + rev: 0.29.3 + hooks: + - id: check-github-workflows + files: ^\.github/workflows/[^/]+$ + types: + - yaml + - id: check-jsonschema + alias: check-github-workflows-timeout + name: Check GitHub Workflows set timeout-minutes + args: + - --builtin-schema + - github-workflows-require-timeout + files: ^\.github/workflows/[^/]+$ + types: + - yaml + - id: check-readthedocs + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v5.0.0' + hooks: + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + - id: file-contents-sorter + files: | + docs/spelling_wordlist.txt| + .gitignore| + .gitattributes + - id: check-case-conflict + - id: check-json + - id: check-xml + - id: check-executables-have-shebangs + - id: check-toml + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: check-added-large-files + - id: check-symlinks + - id: debug-statements + - id: detect-aws-credentials + args: ['--allow-missing-credentials'] + - id: detect-private-key + exclude: ^examples/ +- repo: https://github.com/asottile/pyupgrade + rev: 'v3.17.0' + hooks: + - id: pyupgrade + args: ['--py39-plus'] + +- repo: https://github.com/codespell-project/codespell.git + rev: v2.3.0 + hooks: + - id: codespell + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.2 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + +- repo: https://github.com/Lucas-C/pre-commit-hooks-markup + rev: v1.0.1 + hooks: + - id: rst-linter + exclude: ^CHANGES\.rst$ + files: >- + ^[^/]+[.]rst$ + +- repo: https://github.com/pre-commit/mirrors-mypy.git + rev: v1.11.2 + hooks: + - id: mypy + alias: mypy-py313 + name: MyPy, for Python 3.13 + additional_dependencies: + - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` + - pytest + - pytest-asyncio + - aiohttp + - zeroconf + args: + - --python-version=3.13 + - --txt-report=.tox/.tmp/.mypy/python-3.13 + - --cobertura-xml-report=.tox/.tmp/.mypy/python-3.13 + - --html-report=.tox/.tmp/.mypy/python-3.13 + pass_filenames: false + - id: mypy + alias: mypy-py39 + name: MyPy, for Python 3.9 + additional_dependencies: + - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` + - pytest + - pytest-asyncio + - aiohttp + - zeroconf + args: + - --python-version=3.9 + - --txt-report=.tox/.tmp/.mypy/python-3.9 + - --cobertura-xml-report=.tox/.tmp/.mypy/python-3.9 + - --html-report=.tox/.tmp/.mypy/python-3.9 + pass_filenames: false + +- repo: https://github.com/rhysd/actionlint.git + rev: v1.7.3 + hooks: + - id: actionlint-docker + args: + - -ignore + - >- # https://github.com/rhysd/actionlint/issues/384 + ^type of expression at "float number value" must be number + but found type string$ + - -ignore + - >- # https://github.com/rhysd/actionlint/pull/380#issuecomment-2325391372 + ^input "attestations" is not defined in action + "pypa/gh-action-pypi-publish@release/v1". available inputs are ".*"$ + +... diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d3c25a7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,25 @@ +--- + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + + jobs: + post_create_environment: + - >- + pip install . + --config-settings=pure-python=true + +python: + install: + - requirements: requirements/doc.txt + +sphinx: + builder: dirhtml + configuration: docs/conf.py + fail_on_warning: true + +... diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..82cb77f --- /dev/null +++ b/.yamllint @@ -0,0 +1,18 @@ +--- + +extends: default + +rules: + indentation: + level: error + indent-sequences: false + truthy: + allowed-values: + - >- + false + - >- + true + - >- # Allow "on" key name in GHA CI/CD workflow definitions + on + +... diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..2333ec1 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,15 @@ +========= +Changelog +========= + +.. + You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/#adding-a-news-entry + we named the news folder "changes". + + WARNING: Don't drop the next directive! + +.. towncrier release notes start diff --git a/CHANGES/.TEMPLATE.rst b/CHANGES/.TEMPLATE.rst new file mode 100644 index 0000000..2879ab2 --- /dev/null +++ b/CHANGES/.TEMPLATE.rst @@ -0,0 +1,90 @@ +{# TOWNCRIER TEMPLATE #} + +*({{ versiondata.date }})* + +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{section}} +{{ underline * section|length }}{% set underline = underlines[1] %} + +{% endif %} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section]%} +{{ definitions[category]['name'] }} +{{ underline * definitions[category]['name']|length }} + +{% if definitions[category]['showcontent'] %} +{% for text, change_note_refs in sections[section][category].items() %} +- {{ text }} + + {{- '\n' * 2 -}} + + {#- + NOTE: Replacing 'e' with 'f' is a hack that prevents Jinja's `int` + NOTE: filter internal implementation from treating the input as an + NOTE: infinite float when it looks like a scientific notation (with a + NOTE: single 'e' char in between digits), raising an `OverflowError`, + NOTE: subsequently. 'f' is still a hex letter so it won't affect the + NOTE: check for whether it's a (short or long) commit hash or not. + Ref: https://github.com/pallets/jinja/issues/1921 + -#} + {%- + set pr_issue_numbers = change_note_refs + | map('lower') + | map('replace', 'e', 'f') + | map('int', default=None) + | select('integer') + | map('string') + | list + -%} + {%- set arbitrary_refs = [] -%} + {%- set commit_refs = [] -%} + {%- with -%} + {%- set commit_ref_candidates = change_note_refs | reject('in', pr_issue_numbers) -%} + {%- for cf in commit_ref_candidates -%} + {%- if cf | length in (7, 8, 40) and cf | int(default=None, base=16) is not none -%} + {%- set _ = commit_refs.append(cf) -%} + {%- else -%} + {%- set _ = arbitrary_refs.append(cf) -%} + {%- endif -%} + {%- endfor -%} + {%- endwith -%} + + {% if pr_issue_numbers %} + *Related issues and pull requests on GitHub:* + :issue:`{{ pr_issue_numbers | join('`, :issue:`') }}`. + {{- '\n' * 2 -}} + {%- endif -%} + + {% if commit_refs %} + *Related commits on GitHub:* + :commit:`{{ commit_refs | join('`, :commit:`') }}`. + {{- '\n' * 2 -}} + {%- endif -%} + + {% if arbitrary_refs %} + *Unlinked references:* + {{ arbitrary_refs | join(', ') }}. + {{- '\n' * 2 -}} + {%- endif -%} + +{% endfor %} +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. + +{% else %} +{% endif %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} +---- +{{ '\n' * 2 }} diff --git a/CHANGES/.gitignore b/CHANGES/.gitignore new file mode 100644 index 0000000..d6409a0 --- /dev/null +++ b/CHANGES/.gitignore @@ -0,0 +1,28 @@ +* +!.TEMPLATE.rst +!.gitignore +!README.rst +!*.bugfix +!*.bugfix.rst +!*.bugfix.*.rst +!*.breaking +!*.breaking.rst +!*.breaking.*.rst +!*.contrib +!*.contrib.rst +!*.contrib.*.rst +!*.deprecation +!*.deprecation.rst +!*.deprecation.*.rst +!*.doc +!*.doc.rst +!*.doc.*.rst +!*.feature +!*.feature.rst +!*.feature.*.rst +!*.misc +!*.misc.rst +!*.misc.*.rst +!*.packaging +!*.packaging.rst +!*.packaging.*.rst diff --git a/CHANGES/README.rst b/CHANGES/README.rst new file mode 100644 index 0000000..ea84227 --- /dev/null +++ b/CHANGES/README.rst @@ -0,0 +1,109 @@ +.. _Adding change notes with your PRs: + +Adding change notes with your PRs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is very important to maintain a log for news of how +updating to the new version of the software will affect +end-users. This is why we enforce collection of the change +fragment files in pull requests as per `Towncrier philosophy`_. + +The idea is that when somebody makes a change, they must record +the bits that would affect end-users only including information +that would be useful to them. Then, when the maintainers publish +a new release, they'll automatically use these records to compose +a change log for the respective version. It is important to +understand that including unnecessary low-level implementation +related details generates noise that is not particularly useful +to the end-users most of the time. And so such details should be +recorded in the Git history rather than a changelog. + +Alright! So how to add a news fragment? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``aiohttp_asyncmdnsresolver`` uses `towncrier `_ +for changelog management. +To submit a change note about your PR, add a text file into the +``CHANGES/`` folder. It should contain an +explanation of what applying this PR will change in the way +end-users interact with the project. One sentence is usually +enough but feel free to add as many details as you feel necessary +for the users to understand what it means. + +**Use the past tense** for the text in your fragment because, +combined with others, it will be a part of the "news digest" +telling the readers **what changed** in a specific version of +the library *since the previous version*. You should also use +reStructuredText syntax for highlighting code (inline or block), +linking parts of the docs or external sites. +If you wish to sign your change, feel free to add ``-- by +:user:`github-username``` at the end (replace ``github-username`` +with your own!). + +Finally, name your file following the convention that Towncrier +understands: it should start with the number of an issue or a +PR followed by a dot, then add a patch type, like ``feature``, +``doc``, ``contrib`` etc., and add ``.rst`` as a suffix. If you +need to add more than one fragment, you may add an optional +sequence number (delimited with another period) between the type +and the suffix. + +In general the name will follow ``..rst`` pattern, +where the categories are: + +- ``bugfix``: A bug fix for something we deemed an improper undesired + behavior that got corrected in the release to match pre-agreed + expectations. +- ``feature``: A new behavior, public APIs. That sort of stuff. +- ``deprecation``: A declaration of future API removals and breaking + changes in behavior. +- ``breaking``: When something public gets removed in a breaking way. + Could be deprecated in an earlier release. +- ``doc``: Notable updates to the documentation structure or build + process. +- ``packaging``: Notes for downstreams about unobvious side effects + and tooling. Changes in the test invocation considerations and + runtime assumptions. +- ``contrib``: Stuff that affects the contributor experience. e.g. + Running tests, building the docs, setting up the development + environment. +- ``misc``: Changes that are hard to assign to any of the above + categories. + +A pull request may have more than one of these components, for example +a code change may introduce a new feature that deprecates an old +feature, in which case two fragments should be added. It is not +necessary to make a separate documentation fragment for documentation +changes accompanying the relevant code changes. + +Examples for adding changelog entries to your Pull Requests +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +File :file:`CHANGES/603.removal.1.rst`: + +.. code-block:: rst + + Dropped Python 3.5 support; Python 3.6 is the minimal supported Python + version -- by :user:`webknjaz`. + +File :file:`CHANGES/550.bugfix.rst`: + +.. code-block:: rst + + Started shipping Windows wheels for the x86 architecture + -- by :user:`Dreamsorcerer`. + +File :file:`CHANGES/553.feature.rst`: + +.. code-block:: rst + + Added support for ``GenericAliases`` (``MultiDict[str]``) under Python 3.9 + and higher -- by :user:`mjpieters`. + +.. tip:: + + See :file:`towncrier.toml` for all available categories + (``tool.towncrier.type``). + +.. _Towncrier philosophy: + https://towncrier.readthedocs.io/en/stable/#philosophy diff --git a/LICENSE b/LICENSE index 261eeb9..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..3060683 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,21 @@ +include .coveragerc +include pyproject.toml +include pytest.ini +include towncrier.toml +include LICENSE +include NOTICE +include CHANGES.rst +include README.rst +graft aiohttp_asyncmdnsresolver +graft packaging +graft docs +graft CHANGES +graft requirements +graft tests +global-exclude *.pyc +global-exclude *.cache +exclude src/aiohttp_asyncmdnsresolver/*.c +exclude src/aiohttp_asyncmdnsresolver/*.html +exclude src/aiohttp_asyncmdnsresolver/*.so +exclude src/aiohttp_asyncmdnsresolver/*.pyd +prune docs/_build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c12004 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +all: test + +.install-deps: $(shell find requirements -type f) + pip install -U -r requirements/dev.txt + pre-commit install + @touch .install-deps + + +.develop: .install-deps $(shell find src/aiohttp_asyncmdnsresolver -type f) + @pip install -e . + @touch .develop + +fmt: +ifdef CI + pre-commit run --all-files --show-diff-on-failure +else + pre-commit run --all-files +endif + +lint: fmt + +test: lint .develop + pytest + + +vtest: lint .develop + pytest -v + + +cov: lint .develop + pytest --cov-report html --cov-report term + @echo "python3 -Im webbrowser file://`pwd`/htmlcov/index.html" + + +doc: doctest doc-spelling + make -C docs html SPHINXOPTS="-W -E --keep-going -n" + @echo "python3 -Im webbrowser file://`pwd`/docs/_build/html/index.html" + + +doctest: .develop + make -C docs doctest SPHINXOPTS="-W -E --keep-going -n" + + +doc-spelling: + make -C docs spelling SPHINXOPTS="-W -E --keep-going -n" diff --git a/README.md b/README.md index d63e971..1665098 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# aiohttp-asyncmdnsresolver \ No newline at end of file +# aiohttp-asyncmdnsresolver diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..022bd97 --- /dev/null +++ b/README.rst @@ -0,0 +1,90 @@ +aiohttp-asyncmdnsresolver +========================= + +The module provides a fast implementation of cached properties for Python 3.9+. + +.. image:: https://github.com/aio-libs/aiohttp_asyncmdnsresolver/actions/workflows/ci-cd.yml/badge.svg + :target: https://github.com/aio-libs/aiohttp_asyncmdnsresolver/actions?query=workflow%3ACI + :align: right + +.. image:: https://codecov.io/gh/aio-libs/aiohttp_asyncmdnsresolver/branch/master/graph/badge.svg + :target: https://codecov.io/gh/aio-libs/aiohttp_asyncmdnsresolver + +.. image:: https://badge.fury.io/py/aiohttp_asyncmdnsresolver.svg + :target: https://badge.fury.io/py/aiohttp_asyncmdnsresolver + + +.. image:: https://readthedocs.org/projects/aiohttp_asyncmdnsresolver/badge/?version=latest + :target: https://aiohttp_asyncmdnsresolver.readthedocs.io + + +.. image:: https://img.shields.io/pypi/pyversions/aiohttp_asyncmdnsresolver.svg + :target: https://pypi.python.org/pypi/aiohttp_asyncmdnsresolver + +.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs:matrix.org + :alt: Matrix Room — #aio-libs:matrix.org + +.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs-space:matrix.org + :alt: Matrix Space — #aio-libs-space:matrix.org + +Introduction +------------ + +This module provides an ``aiohttp`` resolver that supports mDNS, which uses the ``zeroconf`` library +to resolve mDNS queries. + +For full documentation please read https://aiohttp_asyncmdnsresolver.readthedocs.io. + +Installation +------------ + +:: + + $ pip install aiohttp_asyncmdnsresolver + + +Quick start +----------- + +:: + + import asyncio + import aiohttp + from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver + + async def main(): + resolver = AsyncMDNSResolver() + connector = aiohttp.TCPConnector(resolver=resolver) + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get('http://example.com') as response: + print(response.status) + async with session.get('http://xxx.local.') as response: + print(response.status) + + asyncio.run(main()) + + +API documentation +----------------- + +The documentation is located at https://aiohttp_asyncmdnsresolver.readthedocs.io. + +Source code +----------- + +The project is hosted on GitHub_ + +Please file an issue on the `bug tracker +`_ if you have found a bug +or have some suggestion in order to improve the library. + + +Authors and License +------------------- + +It's *Apache 2* licensed and freely available. + + +.. _GitHub: https://github.com/aio-libs/aiohttp_asyncmdnsresolver diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..e9f94ad --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,230 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiohttp_asyncmdnsresolver.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiohttp_asyncmdnsresolver.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/aiohttp_asyncmdnsresolver" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiohttp_asyncmdnsresolver" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." + +spelling: + $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling + @echo + @echo "Build finished." diff --git a/docs/_static/README.txt b/docs/_static/README.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..71236d9 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,36 @@ +.. _aiohttp_asyncmdnsresolver-api: + +========= +Reference +========= + +.. module:: aiohttp_asyncmdnsresolver.api + +The only public *aiohttp_asyncmdnsresolver.api* class is :class:`AsyncMDNSResolver`: + +.. doctest:: + + >>> from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver + + +.. class:: AsyncMDNSResolver(*args*, *, async_zeroconf, timeout, **kwargs) + + This class functions the same as :class:`aiohttp.AsyncResolver` + but with the added ability to resolve mDNS queries. + + :param ``AsyncZeroconf`` async_zeroconf: If an ``AsyncZeroconf`` instance is + passed, it will be used to resolve mDNS queries. If not, a new + instance will be created. + + :param float timeout: The timeout for the mDNS query in seconds. If not provided + the default timeout is 5 seconds. + + Example:: + + from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver + + resolver = AsyncMDNSResolver() + connector = aiohttp.TCPConnector(resolver=resolver) + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get("http://KNKSADE41945.local.") as response: + print(response.status) diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 0000000..691e54d --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1,18 @@ +.. _aiohttp_asyncmdnsresolver_changes: + +========= +Changelog +========= + +.. only:: not is_release + + To be included in v\ |release| (if present) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + .. towncrier-draft-entries:: |release| [UNRELEASED DRAFT] + + Released versions + ^^^^^^^^^^^^^^^^^ + +.. include:: ../CHANGES.rst + :start-after: .. towncrier release notes start diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..20a1f43 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +# +# aiohttp_asyncmdnsresolver documentation build configuration file, created by +# sphinx-quickstart on Mon Aug 29 19:55:36 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +import os +import re +from pathlib import Path + +PROJECT_ROOT_DIR = Path(__file__).parents[1].resolve() +IS_RELEASE_ON_RTD = ( + os.getenv("READTHEDOCS", "False") == "True" + and os.environ["READTHEDOCS_VERSION_TYPE"] == "tag" +) +if IS_RELEASE_ON_RTD: + tags.add("is_release") # noqa: F821 + + +_docs_path = Path(__file__).parent +_version_path = _docs_path / "../src/aiohttp_asyncmdnsresolver/__init__.py" + +with _version_path.open() as fp: + try: + _version_info = re.search( + r"^__version__ = \"" + r"(?P\d+)" + r"\.(?P\d+)" + r"\.(?P\d+)" + r"(?P.*)?\"$", + fp.read(), + re.M, + ).groupdict() + except IndexError: + raise RuntimeError("Unable to determine version.") + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + # stdlib-party extensions: + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.viewcode", + # Third-party extensions: + "alabaster", + "sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive + "myst_parser", # extended markdown; https://pypi.org/project/myst-parser/ +] + + +try: + import sphinxcontrib.spelling # noqa + + extensions.append("sphinxcontrib.spelling") +except ImportError: + pass + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "aiohttp": ("https://docs.aiohttp.org/en/stable/", None), +} + + +# Add any paths that contain templates here, relative to this directory. +# templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# -- Project information ----------------------------------------------------- + +github_url = "https://github.com" +github_repo_org = "aio-libs" +github_repo_name = "aiohttp_asyncmdnsresolver" +github_repo_slug = f"{github_repo_org}/{github_repo_name}" +github_repo_url = f"{github_url}/{github_repo_slug}" +github_sponsors_url = f"{github_url}/sponsors" + +project = github_repo_name +copyright = f"{project} contributors and aio-libs team" +author = "aio-libs team" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "{major}.{minor}".format(**_version_info) +# The full version, including alpha/beta/rc tags. +release = "{major}.{minor}.{patch}-{tag}".format(**_version_info) + +rst_epilog = f""" +.. |project| replace:: {project} +""" # pylint: disable=invalid-name + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# -- Extension configuration ------------------------------------------------- + +# -- Options for extlinks extension --------------------------------------- +extlinks = { + "issue": (f"{github_repo_url}/issues/%s", "#%s"), + "pr": (f"{github_repo_url}/pull/%s", "PR #%s"), + "commit": (f"{github_repo_url}/commit/%s", "%s"), + "gh": (f"{github_url}/%s", "GitHub: %s"), + "user": (f"{github_sponsors_url}/%s", "@%s"), +} + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +html_theme_options = { + "description": "aiohttp_asyncmdnsresolver", + "github_user": "aio-libs", + "github_repo": "aiohttp_asyncmdnsresolver", + "github_button": True, + "github_type": "star", + "github_banner": True, + "codecov_button": True, + "pre_bg": "#FFF6E5", + "note_bg": "#E5ECD1", + "note_border": "#BFCF8C", + "body_text": "#482C0A", + "sidebar_text": "#49443E", + "sidebar_header": "#4B4032", + "sidebar_collapse": False, +} + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = 'aiohttp_asyncmdnsresolver v0.1.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +html_sidebars = { + "**": [ + "about.html", + "navigation.html", + "searchbox.html", + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "aiohttp_asyncmdnsresolverdoc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "aiohttp_asyncmdnsresolver.tex", + "aiohttp_asyncmdnsresolver Documentation", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + master_doc, + "aiohttp_asyncmdnsresolver", + "aiohttp_asyncmdnsresolver Documentation", + [author], + 1, + ) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "aiohttp_asyncmdnsresolver", + "aiohttp_asyncmdnsresolver Documentation", + author, + "aiohttp_asyncmdnsresolver", + "One line description of project.", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False + +default_role = "any" +nitpicky = True +nitpick_ignore = [ + ("envvar", "TMPDIR"), +] + +# -- Options for towncrier_draft extension ----------------------------------- + +towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-version', 'sphinx-release' +towncrier_draft_include_empty = True +towncrier_draft_working_directory = PROJECT_ROOT_DIR +# Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd diff --git a/docs/contributing/guidelines.rst b/docs/contributing/guidelines.rst new file mode 100644 index 0000000..f0413b8 --- /dev/null +++ b/docs/contributing/guidelines.rst @@ -0,0 +1,28 @@ +----------------- +Contributing docs +----------------- + +We use Sphinx_ to generate our docs website. You can trigger +the process locally by executing: + + .. code-block:: shell-session + + $ make doc + +It is also integrated with `Read The Docs`_ that builds and +publishes each commit to the main branch and generates live +docs previews for each pull request. + +The sources of the Sphinx_ documents use reStructuredText as a +de-facto standard. But in order to make contributing docs more +beginner-friendly, we've integrated `MyST parser`_ allowing us +to also accept new documents written in an extended version of +Markdown that supports using Sphinx directives and roles. `Read +the docs `_ to learn more on how to use it. + +.. _MyST docs: https://myst-parser.readthedocs.io/en/latest/using/intro.html#writing-myst-in-sphinx +.. _MyST parser: https://pypi.org/project/myst-parser/ +.. _Read The Docs: https://readthedocs.org +.. _Sphinx: https://www.sphinx-doc.org + +.. include:: ../../CHANGES/README.rst diff --git a/docs/contributing/release_guide.rst b/docs/contributing/release_guide.rst new file mode 100644 index 0000000..9a44829 --- /dev/null +++ b/docs/contributing/release_guide.rst @@ -0,0 +1,105 @@ +************* +Release Guide +************* + +Welcome to the |project| Release Guide! + +This page contains information on how to release a new version +of |project| using the automated Continuous Delivery pipeline. + +.. tip:: + + The intended audience for this document is maintainers + and core contributors. + + +Pre-release activities +====================== + +1. Check if there are any open Pull Requests that could be + desired in the upcoming release. If there are any — merge + them. If some are incomplete, try to get them ready. + Don't forget to review the enclosed change notes per our + guidelines. +2. Visually inspect the draft section of the :ref:`Changelog` + page. Make sure the content looks consistent, uses the same + writing style, targets the end-users and adheres to our + documented guidelines. + Most of the changelog sections will typically use the past + tense or another way to relay the effect of the changes for + the users, since the previous release. + It should not target core contributors as the information + they are normally interested in is already present in the + Git history. + Update the changelog fragments if you see any problems with + this changelog section. +3. Optionally, test the previously published nightlies, that are + available through GitHub Actions CI/CD artifacts, locally. +4. If you are satisfied with the above, inspect the changelog + section categories in the draft. Presence of the breaking + changes or features will hint you what version number + segment to bump for the release. +5. Update the hardcoded version string in :file:`aiohttp_asyncmdnsresolver/__init__.py`. + Generate a new changelog from the fragments, and commit it + along with the fragments removal and the Python module changes. + Use the following commands, don't prepend a leading-``v`` before + the version number. Just use the raw version number as per + :pep:`440`. + + .. code-block:: shell-session + + [dir:aiohttp_asyncmdnsresolver] $ aiohttp_asyncmdnsresolver/__init__.py + [dir:aiohttp_asyncmdnsresolver] $ python -m towncrier build \ + -- --version 'VERSION_WITHOUT_LEADING_V' + [dir:aiohttp_asyncmdnsresolver] $ git commit -v CHANGES{.rst,/} aiohttp_asyncmdnsresolver/__init__.py + +.. seealso:: + + :ref:`Adding change notes with your PRs` + Writing beautiful changelogs for humans + + +The release stage +================= + +1. Tag the commit with version and changelog changes, created + during the preparation stage. If possible, make it GPG-signed. + Prepend a leading ``v`` before the version number for the tag + name. Add an extra sentence describing the release contents, + in a few words. + + .. code-block:: shell-session + + [dir:aiohttp_asyncmdnsresolver] $ git tag \ + -s 'VERSION_WITH_LEADING_V' \ + -m 'VERSION_WITH_LEADING_V' \ + -m 'This release does X and Y.' + + +2. Push that tag to the upstream repository, which ``origin`` is + considered to be in the example below. + + .. code-block:: shell-session + + [dir:aiohttp_asyncmdnsresolver] $ git push origin 'VERSION_WITH_LEADING_V' + +3. You can open the `GitHub Actions CI/CD workflow page `_ in your web browser to monitor the + progress. But generally, you don't need to babysit the CI. +4. Check that web page or your email inbox for the notification + with an approval request. GitHub will send it when it reaches + the final "publishing" job. +5. Approve the deployment and wait for the CD workflow to complete. +6. Verify that the following things got created: + - a PyPI release + - a Git tag + - a GitHub Releases page +7. Tell everyone you released a new version of |project| :) + Depending on your mental capacity and the burnout stage, you + are encouraged to post the updates in issues asking for the + next release, contributed PRs, Bluesky, Twitter etc. You can + also call out prominent contributors and thank them! + + +.. _GitHub Actions CI/CD workflow: + https://github.com/aio-libs/aiohttp_asyncmdnsresolver/actions/workflows/ci-cd.yml diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..4d09eaf --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,74 @@ +.. aiohttp_asyncmdnsresolver documentation master file, created by + sphinx-quickstart on Mon Aug 29 19:55:36 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +aiohttp-asyncmdnsresolver +========================= + +This module provides an ``aiohttp`` resolver that supports mDNS, which uses the ``zeroconf`` library +to resolve mDNS queries. + +Introduction +------------ + +Usage +----- + +The API provides a single ``AsyncMDNSResolver`` class that can be +used to resolve mDNS queries and fallback to ``AsyncResolver`` for +non-MDNS hosts. + +API documentation +----------------- + +Open :ref:`aiohttp_asyncmdnsresolver-api` for reading full list of available methods. + +Source code +----------- + +The project is hosted on GitHub_ + +Please file an issue on the `bug tracker +`_ if you have found a bug +or have some suggestion in order to improve the library. + +Authors and License +------------------- + +It's *Apache 2* licensed and freely available. + + + +Contents: + +.. toctree:: + :maxdepth: 2 + + api + +.. toctree:: + :caption: What's new + + changes + +.. toctree:: + :caption: Contributing + + contributing/guidelines + +.. toctree:: + :caption: Maintenance + + contributing/release_guide + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + +.. _GitHub: https://github.com/aio-libs/aiohttp_asyncmdnsresolver diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..ca4e630 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,281 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aiohttp_asyncmdnsresolver.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aiohttp_asyncmdnsresolver.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "epub3" ( + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +if "%1" == "dummy" ( + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end +) + +:end diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 0000000..1a60db3 --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,52 @@ +Bluesky +Bugfixes +Changelog +Codecov +Cython +GPG +IPv +PRs +PYX +Towncrier +Twitter +UTF +aiohttp +aiohttp_asyncmdnsresolver +backend +boolean +booleans +bools +changelog +changelogs +config +de +decodable +dev +dists +downstreams +facto +glibc +google +hardcoded +hostnames +macOS +mailto +manylinux +multi +nightlies +pre +rc +reStructuredText +reencoding +requote +requoting +runtimes +sdist +src +subclass +subclasses +subcomponent +svetlov +uncompiled +unobvious +v1 diff --git a/examples/run.py b/examples/run.py new file mode 100644 index 0000000..9aed164 --- /dev/null +++ b/examples/run.py @@ -0,0 +1,14 @@ +import asyncio +import aiohttp +from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver + + +async def main(): + resolver = AsyncMDNSResolver() + connector = aiohttp.TCPConnector(resolver=resolver) + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get("http://KNKSADE41945.local.") as response: + print(response.status) + + +asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5506f12 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "aiohttp-asyncmdnsresolver" +dependencies = ["aiodns>=3.2.0", "aiohttp>=3.10.0", "zeroconf>=0.132.0"] +description = "An async resolver for aiohttp that supports MDNS" +dynamic = ["version"] +license = {file = "LICENSE"} +readme = "README.rst" +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Internet :: WWW/HTTP", + "Framework :: AsyncIO", + "Framework :: aiohttp", + "Development Status :: 5 - Production/Stable" +] + +[project.urls] +Homepage = "https://github.com/aio-libs/aiohttp-asyncmdnsresolver" +Issues = "https://github.com/aio-libs/aiohttp-asyncmdnsresolver/issues" + +[tool.setuptools] +packages = ["aiohttp_asyncmdnsresolver"] + +[tool.setuptools.package-dir] +"" = "src" + +[tool.setuptools.dynamic] +version = {attr = "aiohttp_asyncmdnsresolver.__version__"} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..1194b90 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,84 @@ +[pytest] +addopts = + # Show 10 slowest invocations: + --durations=10 + + # Report all the things == -rxXs: + -ra + + # Show values of the local vars in errors/tracebacks: + --showlocals + + # Autocollect and invoke the doctests from all modules: + # https://docs.pytest.org/en/stable/doctest.html + --doctest-modules + + # Pre-load the `pytest-cov` plugin early: + -p pytest_cov + + # `pytest-cov`: + --cov + --cov-config=.coveragerc + --cov-context=test + --no-cov-on-fail + + # Fail on config parsing warnings: + # --strict-config + + # Fail on non-existing markers: + # * Deprecated since v6.2.0 but may be reintroduced later covering a + # broader scope: + # --strict + # * Exists since v4.5.0 (advised to be used instead of `--strict`): + --strict-markers + +doctest_optionflags = ALLOW_UNICODE ELLIPSIS + +# Marks tests with an empty parameterset as xfail(run=False) +empty_parameter_set_mark = xfail + +faulthandler_timeout = 30 + +filterwarnings = + error + + # FIXME: drop this once `pytest-cov` is updated. + # Ref: https://github.com/pytest-dev/pytest-cov/issues/557 + ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning + + # https://github.com/pytest-dev/pytest/issues/10977 and https://github.com/pytest-dev/pytest/pull/10894 + ignore:ast\.(Num|NameConstant|Str) is deprecated and will be removed in Python 3\.14; use ast\.Constant instead:DeprecationWarning:_pytest + ignore:Attribute s is deprecated and will be removed in Python 3\.14; use value instead:DeprecationWarning:_pytest + +# https://docs.pytest.org/en/stable/usage.html#creating-junitxml-format-files +junit_duration_report = call +# xunit1 contains more metadata than xunit2 so it's better for CI UIs: +junit_family = xunit1 +junit_logging = all +junit_log_passing_tests = true +junit_suite_name = aiohttp_asyncmdnsresolver_test_suite + +# A mapping of markers to their descriptions allowed in strict mode: +markers = + +minversion = 3.8.2 + +# Optimize pytest's lookup by restricting potentially deep dir tree scan: +norecursedirs = + build + dist + docs + requirements + venv + virtualenv + aiohttp_asyncmdnsresolver.egg-info + .cache + .eggs + .git + .github + .tox + *.egg + +testpaths = tests/ + +xfail_strict = true diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..2a4069d --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,2 @@ +-r test.txt +-r towncrier.txt diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt new file mode 100644 index 0000000..3bb4a55 --- /dev/null +++ b/requirements/doc-spelling.txt @@ -0,0 +1,2 @@ +-r doc.txt +sphinxcontrib-spelling==8.0.1; platform_system!="Windows" # We only use it in Azure CI diff --git a/requirements/doc.txt b/requirements/doc.txt new file mode 100644 index 0000000..c72a429 --- /dev/null +++ b/requirements/doc.txt @@ -0,0 +1,4 @@ +-r towncrier.txt +myst-parser >= 0.10.0 +sphinx==8.1.3 +sphinxcontrib-towncrier diff --git a/requirements/lint.txt b/requirements/lint.txt new file mode 100644 index 0000000..e88d271 --- /dev/null +++ b/requirements/lint.txt @@ -0,0 +1 @@ +pre-commit==4.0.1 diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..f768631 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,4 @@ +covdefaults +pytest==8.3.4 +pytest-asyncio==0.25.1 +pytest-cov>=2.3.1 diff --git a/requirements/towncrier.txt b/requirements/towncrier.txt new file mode 100644 index 0000000..409f3a3 --- /dev/null +++ b/requirements/towncrier.txt @@ -0,0 +1 @@ +towncrier==23.11.0 diff --git a/src/aiohttp_asyncmdnsresolver/__init__.py b/src/aiohttp_asyncmdnsresolver/__init__.py new file mode 100644 index 0000000..33de284 --- /dev/null +++ b/src/aiohttp_asyncmdnsresolver/__init__.py @@ -0,0 +1,4 @@ +"""aiohttp_asyncmdnsresolver: An async resolver that support MDNS.""" + +__version__ = "0.0.0.dev0" +__all__ = () diff --git a/src/aiohttp_asyncmdnsresolver/_impl.py b/src/aiohttp_asyncmdnsresolver/_impl.py new file mode 100644 index 0000000..2e8a548 --- /dev/null +++ b/src/aiohttp_asyncmdnsresolver/_impl.py @@ -0,0 +1,121 @@ +"""Asynchronous DNS resolver using mDNS for `aiohttp`.""" + +from __future__ import annotations + +import socket +from typing import Any +from zeroconf import IPVersion +from zeroconf.asyncio import AsyncZeroconf, AsyncServiceInfo +from aiohttp.resolver import AsyncResolver, ResolveResult +from ipaddress import IPv4Address, IPv6Address + + +class IPv6orIPv4HostResolver(AsyncServiceInfo): + """Resolve a host name to an IP address.""" + + @property + def _is_complete(self) -> bool: + """The ServiceInfo has all expected properties.""" + return bool(self._ipv4_addresses) or bool(self._ipv6_addresses) + + +class IPv6HostResolver(AsyncServiceInfo): + """Resolve a host name to an IP address.""" + + @property + def _is_complete(self) -> bool: + """The ServiceInfo has all expected properties.""" + return bool(self._ipv6_addresses) + + +class IPv4HostResolver(AsyncServiceInfo): + """Resolve a host name to an IP address.""" + + @property + def _is_complete(self) -> bool: + """The ServiceInfo has all expected properties.""" + return bool(self._ipv4_addresses) + + +DEFAULT_TIMEOUT = 5.0 + +_FAMILY_TO_RESOLVER_CLASS = { + socket.AF_INET: IPv4HostResolver, + socket.AF_INET6: IPv6HostResolver, + socket.AF_UNSPEC: IPv6orIPv4HostResolver, +} +_FAMILY_TO_IP_VERSION = { + socket.AF_INET: IPVersion.V4Only, + socket.AF_INET6: IPVersion.V6Only, + socket.AF_UNSPEC: IPVersion.All, +} +_IP_VERSION_TO_FAMILY = { + 4: socket.AF_INET, + 6: socket.AF_INET6, +} +_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV + + +def _to_resolve_result( + hostname: str, port: int, ipaddress: IPv4Address | IPv6Address +) -> ResolveResult: + """Convert an IP address to a ResolveResult.""" + return ResolveResult( + hostname=hostname, + host=ipaddress.compressed, + port=port, + family=_IP_VERSION_TO_FAMILY[ipaddress.version], + proto=0, + flags=_NUMERIC_SOCKET_FLAGS, + ) + + +class AsyncMDNSResolver(AsyncResolver): + """Use the `aiodns`/`zeroconf` packages to make asynchronous DNS lookups.""" + + def __init__( + self, + *args: Any, + async_zeroconf: AsyncZeroconf | None = None, + timeout: float | None = None, + **kwargs: Any, + ) -> None: + """Initialize the resolver.""" + super().__init__(*args, **kwargs) + self._timeout = timeout or DEFAULT_TIMEOUT + self._aiozc_owner = async_zeroconf is None + self._aiozc = async_zeroconf or AsyncZeroconf() + + async def resolve( + self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET + ) -> list[ResolveResult]: + """Resolve a host name to an IP address.""" + if host.endswith(".local") or host.endswith(".local."): + return await self._resolve_mdns(host, port, family) + return await super().resolve(host, port, family) + + async def _resolve_mdns( + self, host: str, port: int, family: socket.AddressFamily + ) -> list[ResolveResult]: + """Resolve a host name to an IP address using mDNS.""" + resolver_class: type[AsyncServiceInfo] = _FAMILY_TO_RESOLVER_CLASS[family] + ip_version: IPVersion = _FAMILY_TO_IP_VERSION[family] + if host[-1] != ".": + host += "." + info = resolver_class(".local.", host, server=host) + if ( + info.load_from_cache(self._aiozc.zeroconf) + or ( + self._timeout + and await info.async_request(self._aiozc.zeroconf, self._timeout * 1000) + ) + ) and (addresses := info.ip_addresses_by_version(ip_version)): + return [_to_resolve_result(host, port, address) for address in addresses] + raise OSError(None, "MDNS lookup failed") + + async def close(self) -> None: + """Close the resolver.""" + if self._aiozc_owner: + await self._aiozc.async_close() + await super().close() + self._aiozc = None # type: ignore[assignment] # break ref cycles early diff --git a/src/aiohttp_asyncmdnsresolver/api.py b/src/aiohttp_asyncmdnsresolver/api.py new file mode 100644 index 0000000..8224c78 --- /dev/null +++ b/src/aiohttp_asyncmdnsresolver/api.py @@ -0,0 +1,5 @@ +"""Public API of the property caching library.""" + +from ._impl import AsyncMDNSResolver + +__all__ = ("AsyncMDNSResolver",) diff --git a/src/aiohttp_asyncmdnsresolver/py.typed b/src/aiohttp_asyncmdnsresolver/py.typed new file mode 100644 index 0000000..dcf2c80 --- /dev/null +++ b/src/aiohttp_asyncmdnsresolver/py.typed @@ -0,0 +1 @@ +# Placeholder diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..34e0e8c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +"""Test conftest.py.""" + +import sys +import asyncio + +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..7a3bfb4 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,9 @@ +"""Test we do not break the public API.""" + +from aiohttp_asyncmdnsresolver import api, _impl + + +def test_api() -> None: + """Verify the public API is accessible.""" + assert api.AsyncMDNSResolver is not None + assert api.AsyncMDNSResolver is _impl.AsyncMDNSResolver diff --git a/tests/test_impl.py b/tests/test_impl.py new file mode 100644 index 0000000..19c84ce --- /dev/null +++ b/tests/test_impl.py @@ -0,0 +1,214 @@ +from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver +from aiohttp.resolver import ResolveResult +import pytest +import pytest_asyncio +from zeroconf.asyncio import AsyncZeroconf +from collections.abc import AsyncGenerator +from unittest.mock import patch +from ipaddress import IPv6Address, IPv4Address +import socket +from aiohttp_asyncmdnsresolver._impl import ( + IPv4HostResolver, + IPv6HostResolver, + IPv6orIPv4HostResolver, +) + + +@pytest_asyncio.fixture +async def resolver() -> AsyncGenerator[AsyncMDNSResolver]: + """Return a resolver.""" + resolver = AsyncMDNSResolver(timeout=0.1) + yield resolver + await resolver.close() + + +@pytest_asyncio.fixture +async def custom_resolver() -> AsyncGenerator[AsyncMDNSResolver]: + """Return a resolver.""" + aiozc = AsyncZeroconf() + resolver = AsyncMDNSResolver(timeout=0.1, async_zeroconf=aiozc) + yield resolver + await resolver.close() + await aiozc.async_close() + + +@pytest.mark.asyncio +async def test_resolve_localhost(resolver: AsyncMDNSResolver) -> None: + """Test the resolve method delegates to AsyncResolver for non MDNS.""" + with patch( + "aiohttp_asyncmdnsresolver._impl.AsyncResolver.resolve", + return_value=[ResolveResult(hostname="localhost", host="127.0.0.1")], # type: ignore[typeddict-item] + ): + results = await resolver.resolve("localhost") + assert results is not None + assert len(results) == 1 + result = results[0] + assert result["hostname"] == "localhost" + assert result["host"] == "127.0.0.1" + + +@pytest.mark.asyncio +async def test_resolve_mdns_name_unspec(resolver: AsyncMDNSResolver) -> None: + """Test the resolve method with unspecified family.""" + with ( + patch.object(IPv6orIPv4HostResolver, "async_request", return_value=True), + patch.object( + IPv6orIPv4HostResolver, + "ip_addresses_by_version", + return_value=[IPv4Address("127.0.0.1"), IPv6Address("::1")], + ), + ): + result = await resolver.resolve("localhost.local", family=socket.AF_UNSPEC) + + assert result is not None + assert len(result) == 2 + assert result[0]["hostname"] == "localhost.local." + assert result[0]["host"] == "127.0.0.1" + assert result[1]["hostname"] == "localhost.local." + assert result[1]["host"] == "::1" + + +@pytest.mark.asyncio +async def test_resolve_mdns_name_unspec_from_cache(resolver: AsyncMDNSResolver) -> None: + """Test the resolve method from_cache.""" + with ( + patch.object(IPv6orIPv4HostResolver, "load_from_cache", return_value=True), + patch.object( + IPv6orIPv4HostResolver, + "ip_addresses_by_version", + return_value=[IPv4Address("127.0.0.1"), IPv6Address("::1")], + ), + ): + result = await resolver.resolve("localhost.local", 80, family=socket.AF_UNSPEC) + + assert result is not None + assert len(result) == 2 + assert result[0]["hostname"] == "localhost.local." + assert result[0]["host"] == "127.0.0.1" + assert result[0]["port"] == 80 + assert result[1]["hostname"] == "localhost.local." + assert result[1]["host"] == "::1" + assert result[1]["port"] == 80 + + +@pytest.mark.asyncio +async def test_resolve_mdns_name_unspec_no_results(resolver: AsyncMDNSResolver) -> None: + """Test the resolve method no results.""" + with ( + patch.object(IPv6orIPv4HostResolver, "async_request", return_value=True), + patch.object( + IPv6orIPv4HostResolver, + "ip_addresses_by_version", + return_value=[], + ), + pytest.raises(OSError, match="MDNS lookup failed"), + ): + await resolver.resolve("localhost.local", family=socket.AF_UNSPEC) + + +@pytest.mark.asyncio +async def test_resolve_mdns_name_unspec_trailing_dot( + resolver: AsyncMDNSResolver, +) -> None: + """Test the resolve method with unspecified family with trailing dot.""" + with ( + patch.object(IPv6orIPv4HostResolver, "async_request", return_value=True), + patch.object( + IPv6orIPv4HostResolver, + "ip_addresses_by_version", + return_value=[IPv4Address("127.0.0.1"), IPv6Address("::1")], + ), + ): + result = await resolver.resolve("localhost.local.", family=socket.AF_UNSPEC) + + assert result is not None + assert len(result) == 2 + assert result[0]["hostname"] == "localhost.local." + assert result[0]["host"] == "127.0.0.1" + assert result[1]["hostname"] == "localhost.local." + assert result[1]["host"] == "::1" + + +@pytest.mark.asyncio +async def test_resolve_mdns_name_af_inet(resolver: AsyncMDNSResolver) -> None: + """Test the resolve method with socket.AF_INET family.""" + with ( + patch.object(IPv4HostResolver, "async_request", return_value=True), + patch.object( + IPv4HostResolver, + "ip_addresses_by_version", + return_value=[IPv4Address("127.0.0.1")], + ), + ): + result = await resolver.resolve("localhost.local", family=socket.AF_INET) + + assert result is not None + assert len(result) == 1 + assert result[0]["hostname"] == "localhost.local." + assert result[0]["host"] == "127.0.0.1" + + +@pytest.mark.asyncio +async def test_resolve_mdns_name_af_inet6(resolver: AsyncMDNSResolver) -> None: + """Test the resolve method with socket.AF_INET6 family.""" + with ( + patch.object(IPv6HostResolver, "async_request", return_value=True), + patch.object( + IPv6HostResolver, + "ip_addresses_by_version", + return_value=[IPv6Address("::1")], + ), + ): + result = await resolver.resolve("localhost.local", family=socket.AF_INET6) + + assert result is not None + assert len(result) == 1 + assert result[0]["hostname"] == "localhost.local." + assert result[0]["host"] == "::1" + + +@pytest.mark.asyncio +async def test_resolve_mdns_passed_in_asynczeroconf( + custom_resolver: AsyncMDNSResolver, +) -> None: + """Test the resolve method with unspecified family with a passed in zeroconf.""" + assert custom_resolver._aiozc_owner is False + assert custom_resolver._aiozc is not None + with ( + patch.object(IPv6orIPv4HostResolver, "async_request", return_value=True), + patch.object( + IPv6orIPv4HostResolver, + "ip_addresses_by_version", + return_value=[IPv4Address("127.0.0.1"), IPv6Address("::1")], + ), + ): + result = await custom_resolver.resolve( + "localhost.local", family=socket.AF_UNSPEC + ) + + assert result is not None + assert len(result) == 2 + assert result[0]["hostname"] == "localhost.local." + assert result[0]["host"] == "127.0.0.1" + assert result[1]["hostname"] == "localhost.local." + assert result[1]["host"] == "::1" + + +@pytest.mark.asyncio +async def test_create_destroy_resolver() -> None: + """Test the resolver can be created and destroyed.""" + aiozc = AsyncZeroconf() + resolver = AsyncMDNSResolver(timeout=0.1, async_zeroconf=aiozc) + await resolver.close() + await aiozc.async_close() + assert resolver._aiozc is None + assert resolver._aiozc_owner is False + + +@pytest.mark.asyncio +async def test_create_destroy_resolver_no_aiozc() -> None: + """Test the resolver can be created and destroyed.""" + resolver = AsyncMDNSResolver(timeout=0.1) + await resolver.close() + assert resolver._aiozc is None + assert resolver._aiozc_owner is True diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 0000000..a371acb --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,68 @@ +[tool.towncrier] + package = "aiohttp_asyncmdnsresolver" + filename = "CHANGES.rst" + directory = "CHANGES/" + title_format = "v{version}" + template = "CHANGES/.TEMPLATE.rst" + issue_format = "{issue}" + + # NOTE: The types are declared because: + # NOTE: - there is no mechanism to override just the value of + # NOTE: `tool.towncrier.type.misc.showcontent`; + # NOTE: - and, we want to declare extra non-default types for + # NOTE: clarity and flexibility. + + [[tool.towncrier.section]] + path = "" + + [[tool.towncrier.type]] + # Something we deemed an improper undesired behavior that got corrected + # in the release to match pre-agreed expectations. + directory = "bugfix" + name = "Bug fixes" + showcontent = true + + [[tool.towncrier.type]] + # New behaviors, public APIs. That sort of stuff. + directory = "feature" + name = "Features" + showcontent = true + + [[tool.towncrier.type]] + # Declarations of future API removals and breaking changes in behavior. + directory = "deprecation" + name = "Deprecations (removal in next major release)" + showcontent = true + + [[tool.towncrier.type]] + # When something public gets removed in a breaking way. Could be + # deprecated in an earlier release. + directory = "breaking" + name = "Removals and backward incompatible breaking changes" + showcontent = true + + [[tool.towncrier.type]] + # Notable updates to the documentation structure or build process. + directory = "doc" + name = "Improved documentation" + showcontent = true + + [[tool.towncrier.type]] + # Notes for downstreams about unobvious side effects and tooling. Changes + # in the test invocation considerations and runtime assumptions. + directory = "packaging" + name = "Packaging updates and notes for downstreams" + showcontent = true + + [[tool.towncrier.type]] + # Stuff that affects the contributor experience. e.g. Running tests, + # building the docs, setting up the development environment. + directory = "contrib" + name = "Contributor-facing changes" + showcontent = true + + [[tool.towncrier.type]] + # Changes that are hard to assign to any of the above categories. + directory = "misc" + name = "Miscellaneous internal changes" + showcontent = true