diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 3c40a2b..fa0bebc 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -39,3 +39,28 @@ jobs: python -m pip install "tox>=4.10" - run: | python -m tox run -r + + windows: + runs-on: windows-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + toxenv: [core, lint] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install "tox>=4.10" + - name: Test with tox + shell: bash + run: | + python_version="${{ matrix.python-version }}" + toxenv="${{ matrix.toxenv }}" + echo "TOXENV=py${python_version}-${toxenv}" | tr -d '.' | tee -a "$GITHUB_ENV" + python -m tox run -r diff --git a/.gitignore b/.gitignore index 10acf35..aef2b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,10 @@ coverage.xml # Sphinx documentation docs/_build/ +# Generated HTML files +*.html +!docs/_build/html/*.html + # PyBuilder target/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de81a1f..89b5d36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,8 +18,19 @@ repos: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - - repo: https://github.com/northisup/pyright-pretty - rev: v0.1.0 + - repo: local hooks: - - id: pyright-pretty - args: [--show-summary] + - id: mypy-local + name: run mypy with all dev dependencies present + entry: mypy -p multiaddr + language: system + always_run: true + pass_filenames: false + - repo: local + hooks: + - id: pyrefly-local + name: run pyrefly typecheck locally + entry: pyrefly check + language: system + always_run: true + pass_filenames: false diff --git a/AUTHORS b/AUTHORS index 261689f..9fc17f0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,4 +1,18 @@ Steven Buss +Alexander Schlarb Fred Thomsen Jesse Weinstein -Alexander Schlarb +mhchia +acul71 +arcinston +pacrob +raulk +robzajac +zixuanzh +hugovk +justheuristic +manusheel +rodvagg +venkat +fredthomsen +web3-bot diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 00ea023..e7d418b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -20,6 +20,7 @@ Report bugs at https://github.com/multiformats/py-multiaddr/issues. If you are reporting a bug, please include: * Your operating system name and version. +* Python version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. @@ -62,13 +63,13 @@ Ready to contribute? Here's how to set up `multiaddr` for local development. 1. Fork the `multiaddr` repo on GitHub. 2. Clone your fork locally:: - $ git clone git@github.com:your_name_here/multiaddr.git + $ git clone git@github.com:your_name_here/py-multiaddr.git -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: +3. Install your local copy into a virtual environment:: - $ mkvirtualenv multiaddr - $ cd multiaddr/ - $ python setup.py develop + $ python -m venv venv + $ source venv/bin/activate # On Windows: venv\Scripts\activate + $ pip install -e ".[dev]" 4. Create a branch for local development:: @@ -76,13 +77,19 @@ Ready to contribute? Here's how to set up `multiaddr` for local development. Now you can make your changes locally. -5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: +5. When you're done making changes, run the development workflow:: - $ flake8 multiaddr tests - $ python setup.py test - $ tox + $ make pr - To get flake8 and tox, just pip install them into your virtualenv. + This will run: clean → fix → lint → typecheck → test + + Or run individual commands:: + + $ make fix # Fix formatting & linting issues with ruff + $ make lint # Run pre-commit hooks on all files + $ make typecheck # Run mypy and pyrefly type checking + $ make test # Run tests with pytest + $ make coverage # Run tests with coverage report 6. Commit your changes and push your branch to GitHub:: @@ -101,13 +108,41 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 2.7, 3.4+ and PyPy3. Check - https://travis-ci.org/multiformats/py-multiaddr/pull_requests - and make sure that the tests pass for all supported Python versions. +3. The pull request should work for Python 3.10+ (Python 3.9 support was dropped). +4. All type checking must pass (mypy and pyrefly). +5. All pre-commit hooks must pass. +6. Code must be formatted with ruff. + +Development Workflow +-------------------- + +The project follows a py-libp2p-style development workflow: + +1. **Clean**: Remove build artifacts +2. **Fix**: Auto-fix formatting and linting issues +3. **Lint**: Run pre-commit hooks +4. **Typecheck**: Run mypy and pyrefly +5. **Test**: Run the test suite + +Use ``make pr`` to run the complete workflow. + +Release Notes +------------- + +When contributing, please add a newsfragment file in the ``newsfragments/`` directory. +See ``newsfragments/README.md`` for details on the format and types. Tips ---- To run a subset of tests:: - $ python -m unittest tests.test_multiaddr + $ python -m pytest tests/test_multiaddr.py + +To run with coverage:: + + $ make coverage + +To build documentation:: + + $ make docs diff --git a/HISTORY.rst b/HISTORY.rst index b369f3e..9922d39 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,23 @@ History .. towncrier release notes start +0.0.10 (2025-6-18) +------------------ + +* Fix Type Issues and add strict type checks using Ruff & Pyright +* Spec updates, Python 3.4- unsupport & custom registries by @ntninja in #59 +* add quic-v1 protocol by @justheuristic in #63 +* Fix/typecheck by @acul71 in #65 +* chore: rm local pyrightconfig.json by @arcinston in #70 + +0.0.9 (2019-12-23) +------------------ + +* Add Multiaddr.__hash__ method for hashable multiaddrs +* Add onion3 address support +* Fix broken reST and links in documentation +* Remove emoji from README.rst + 0.0.7 (2019-5-8) ---------------- diff --git a/MANIFEST.in b/MANIFEST.in index 2449534..f2794e0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,15 @@ include AUTHORS include CONTRIBUTING.rst include HISTORY.rst -include LICENSE +include LICENSE-APACHE2 +include LICENSE-MIT include README.rst recursive-include tests * + +global-include *.pyi + recursive-exclude * __pycache__ recursive-exclude * *.py[co] - -recursive-include docs *.rst conf.py Makefile make.bat +prune .tox +prune venv* diff --git a/Makefile b/Makefile index b3292a2..5520533 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc clean-build docs clean +.PHONY: clean-pyc clean-build docs clean help pr define BROWSER_PYSCRIPT import os, webbrowser, sys try: @@ -11,6 +11,26 @@ endef export BROWSER_PYSCRIPT BROWSER := python -c "$$BROWSER_PYSCRIPT" +help: + @echo "Available commands:" + @echo "clean-build - remove build artifacts" + @echo "clean-pyc - remove Python file artifacts" + @echo "clean-test - remove test artifacts" + @echo "clean - run clean-build, clean-pyc, and clean-test" + @echo "setup - install development requirements" + @echo "fix - fix formatting & linting issues with ruff" + @echo "lint - run pre-commit hooks on all files" + @echo "typecheck - run mypy and pyrefly type checking" + @echo "test - run tests quickly with the default Python" + @echo "coverage - run tests with coverage report" + @echo "docs-ci - generate docs for CI" + @echo "docs - generate docs and open in browser" + @echo "servedocs - serve docs with live reload" + @echo "authors - generate AUTHORS file from git" + @echo "dist - build package and show contents" + @echo "notes - consume towncrier newsfragments and update release notes (requires bump parameter)" + @echo "pr - run clean, lint, and test (everything needed before creating a PR)" + clean: clean-build clean-pyc clean-test clean-build: @@ -32,16 +52,22 @@ clean-test: rm -fr htmlcov/ setup: - pip install -r requirements_dev.txt + pip install -e ".[dev]" lint: pre-commit run --all-files +fix: + python -m ruff check --fix + +typecheck: + pre-commit run mypy-local --all-files && pre-commit run pyrefly-local --all-files + test: python -m pytest tests coverage: - coverage run --source multiaddr setup.py test + coverage run --source multiaddr -m pytest tests coverage report -m coverage html $(BROWSER) htmlcov/index.html @@ -88,6 +114,9 @@ release: check-bump clean git push upstream && git push upstream --tags twine upload dist/* +pr: clean fix lint typecheck test + @echo "PR preparation complete! All checks passed." + # helpers # verify that newsfragments are valid and towncrier can build them diff --git a/README.rst b/README.rst index b896e6e..d612ba0 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ py-multiaddr .. image:: https://img.shields.io/pypi/v/multiaddr.svg :target: https://pypi.python.org/pypi/multiaddr -.. image:: https://api.travis-ci.com/multiformats/py-multiaddr.svg?branch=master - :target: https://travis-ci.com/multiformats/py-multiaddr +.. image:: https://github.com/multiformats/py-multiaddr/actions/workflows/ci.yml/badge.svg + :target: https://github.com/multiformats/py-multiaddr/actions .. image:: https://codecov.io/github/multiformats/py-multiaddr/coverage.svg?branch=master :target: https://codecov.io/github/multiformats/py-multiaddr?branch=master @@ -24,6 +24,19 @@ py-multiaddr .. contents:: :local: +Installation +============ + +.. code-block:: bash + + pip install multiaddr + +Requirements +------------ + +- Python 3.10+ +- trio (for async DNS resolution) + Usage ===== @@ -217,11 +230,23 @@ Multiaddr provides thin waist address validation functionality to process multia For comprehensive examples including error handling, practical usage scenarios, and detailed network interface information, see the `thin waist examples `_ in the examples directory. +Features +======== + +- **Multiaddr Protocol Support**: Full support for the multiaddr specification +- **DNS Resolution**: Async DNS and DNSADDR resolution with trio +- **Thin Waist Validation**: Network interface discovery and wildcard expansion +- **Protocol Support**: IPv4, IPv6, TCP, UDP, DNS, DNS4, DNS6, DNSADDR, p2p, p2p-circuit, onion, onion3, quic, tls, and more +- **Type Safety**: Full type hints and mypy support +- **Modern Python**: Python 3.10+ support with modern tooling + Maintainers =========== Original author: `@sbuss`_. +Current maintainers: `@acul71`_, `@pacrob`_, `@manusheel`_. + Contribute ========== @@ -230,6 +255,23 @@ Contributions welcome. Please check out `the issues`_. Check out our `contributing document`_ for more information on how we work, and about contributing in general. Please be aware that all interactions related to multiformats are subject to the IPFS `Code of Conduct`_. +Development +----------- + +For development setup, see :doc:`contributing`. + +.. code-block:: bash + + # Clone the repository + git clone https://github.com/multiformats/py-multiaddr.git + cd py-multiaddr + + # Install in development mode + pip install -e ".[dev]" + + # Run the development workflow + make pr + License ======= @@ -245,3 +287,6 @@ Dual-licensed: .. _MIT: LICENSE-MIT .. _Apache 2: LICENSE-APACHE2 .. _`@sbuss`: https://github.com/sbuss +.. _`@acul71`: https://github.com/acul71 +.. _`@pacrob`: https://github.com/pacrob +.. _`@manusheel`: https://github.com/manusheel diff --git a/docs/contributing.rst b/docs/contributing.rst index e582053..07f5cf5 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1 +1,4 @@ +Contributing +============ + .. include:: ../CONTRIBUTING.rst diff --git a/multiaddr/__init__.py b/multiaddr/__init__.py index d3eafe3..9ce813b 100755 --- a/multiaddr/__init__.py +++ b/multiaddr/__init__.py @@ -2,4 +2,4 @@ __author__ = "Steven Buss" __email__ = "steven.buss@gmail.com" -__version__ = "0.0.9" +__version__ = "0.0.10" diff --git a/multiaddr/codecs/cid.py b/multiaddr/codecs/cid.py index 99550d5..0258d93 100644 --- a/multiaddr/codecs/cid.py +++ b/multiaddr/codecs/cid.py @@ -1,4 +1,5 @@ import logging +from typing import Any import base58 import cid @@ -93,7 +94,7 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string: str) -> bytes: + def to_bytes(self, proto: Any, string: str) -> bytes: """Convert a CID string to its binary representation.""" if not string: raise ValueError("CID string cannot be empty") @@ -122,7 +123,7 @@ def to_bytes(self, proto, string: str) -> bytes: logger.debug(f"[DEBUG CID to_bytes] Failed to parse as CIDv1: {e}") raise ValueError(f"Invalid CID: {string}") - def to_string(self, proto, buf: bytes) -> str: + def to_string(self, proto: Any, buf: bytes) -> str: """Convert a binary CID to its string representation.""" if not buf: raise ValueError("CID buffer cannot be empty") diff --git a/multiaddr/codecs/domain.py b/multiaddr/codecs/domain.py index cdd04bf..1d41e0a 100644 --- a/multiaddr/codecs/domain.py +++ b/multiaddr/codecs/domain.py @@ -1,3 +1,5 @@ +from typing import Any + import idna from ..exceptions import BinaryParseError @@ -11,7 +13,7 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string: str) -> bytes: + def to_bytes(self, proto: Any, string: str) -> bytes: """Convert a domain name string to its binary representation (UTF-8), validating with IDNA.""" if not string: @@ -23,7 +25,7 @@ def to_bytes(self, proto, string: str) -> bytes: except idna.IDNAError as e: raise ValueError(f"Invalid domain name: {e!s}") - def to_string(self, proto, buf: bytes) -> str: + def to_string(self, proto: Any, buf: bytes) -> str: """Convert a binary domain name to its string representation (UTF-8), validating with IDNA.""" if not buf: @@ -37,13 +39,13 @@ def to_string(self, proto, buf: bytes) -> str: raise BinaryParseError(f"Invalid domain name encoding: {e!s}", buf, proto.name, e) -def to_bytes(proto, string): +def to_bytes(proto: Any, string: str) -> bytes: # Validate using IDNA, but store as UTF-8 idna.encode(string, uts46=True) return string.encode("utf-8") -def to_string(proto, buf): +def to_string(proto: Any, buf: bytes) -> str: string = buf.decode("utf-8") # Validate using IDNA idna.encode(string, uts46=True) diff --git a/multiaddr/codecs/fspath.py b/multiaddr/codecs/fspath.py index d851df2..e07445e 100644 --- a/multiaddr/codecs/fspath.py +++ b/multiaddr/codecs/fspath.py @@ -1,5 +1,6 @@ import logging import urllib.parse +from typing import Any from ..exceptions import BinaryParseError from . import LENGTH_PREFIXED_VAR_SIZE, CodecBase @@ -14,7 +15,7 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string: str) -> bytes: + def to_bytes(self, proto: Any, string: str) -> bytes: """Convert a filesystem path to its binary representation.""" logger.debug(f"[DEBUG fspath.to_bytes] input value: {string}") if not string: @@ -35,12 +36,12 @@ def to_bytes(self, proto, string: str) -> bytes: # Encode as UTF-8 encoded = string.encode("utf-8") - logger.debug(f"[DEBUG fspath.to_bytes] encoded bytes: {encoded}") + logger.debug(f"[DEBUG fspath.to_bytes] encoded bytes: {encoded!r}") return encoded - def to_string(self, proto, buf: bytes) -> str: + def to_string(self, proto: Any, buf: bytes) -> str: """Convert a binary filesystem path to its string representation.""" - logger.debug(f"[DEBUG fspath.to_string] input bytes: {buf}") + logger.debug(f"[DEBUG fspath.to_string] input bytes: {buf!r}") if not buf: raise ValueError("Path buffer cannot be empty") diff --git a/multiaddr/codecs/ip4.py b/multiaddr/codecs/ip4.py index b3ddfa3..ad0ef5f 100644 --- a/multiaddr/codecs/ip4.py +++ b/multiaddr/codecs/ip4.py @@ -1,3 +1,5 @@ +from typing import Any + import netaddr from ..codecs import CodecBase @@ -11,10 +13,10 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string): + def to_bytes(self, proto: Any, string: str) -> bytes: return netaddr.IPAddress(string, version=4).packed - def to_string(self, proto, buf): + def to_string(self, proto: Any, buf: bytes) -> str: try: return str(netaddr.IPAddress(int.from_bytes(buf, byteorder="big"), version=4)) except (ValueError, netaddr.AddrFormatError): diff --git a/multiaddr/codecs/ip6.py b/multiaddr/codecs/ip6.py index 5ccf420..b347083 100644 --- a/multiaddr/codecs/ip6.py +++ b/multiaddr/codecs/ip6.py @@ -1,3 +1,5 @@ +from typing import Any + import netaddr from ..codecs import CodecBase @@ -10,8 +12,8 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string): + def to_bytes(self, proto: Any, string: str) -> bytes: return netaddr.IPAddress(string, version=6).packed - def to_string(self, proto, buf): + def to_string(self, proto: Any, buf: bytes) -> str: return str(netaddr.IPAddress(int.from_bytes(buf, byteorder="big"), version=6)) diff --git a/multiaddr/codecs/onion.py b/multiaddr/codecs/onion.py index 86a5d1e..785ab4a 100644 --- a/multiaddr/codecs/onion.py +++ b/multiaddr/codecs/onion.py @@ -1,5 +1,6 @@ import base64 import binascii +from typing import Any from ..codecs import CodecBase from ..exceptions import BinaryParseError @@ -12,7 +13,7 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string): + def to_bytes(self, proto: Any, string: str) -> bytes: try: addr, port = string.split(":", 1) if addr.endswith(".onion"): @@ -32,7 +33,7 @@ def to_bytes(self, proto, string): except (ValueError, UnicodeEncodeError, binascii.Error) as e: raise BinaryParseError(str(e), string.encode(), proto) - def to_string(self, proto, buf): + def to_string(self, proto: Any, buf: bytes) -> str: try: if len(buf) != 12: # 10 bytes for address + 2 bytes for port raise ValueError("Invalid onion address length") diff --git a/multiaddr/codecs/onion3.py b/multiaddr/codecs/onion3.py index c53019b..15e4732 100644 --- a/multiaddr/codecs/onion3.py +++ b/multiaddr/codecs/onion3.py @@ -1,5 +1,6 @@ import base64 import binascii +from typing import Any from ..codecs import CodecBase from ..exceptions import BinaryParseError @@ -12,7 +13,7 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string): + def to_bytes(self, proto: Any, string: str) -> bytes: try: addr, port = string.split(":", 1) if addr.endswith(".onion"): @@ -35,7 +36,7 @@ def to_bytes(self, proto, string): except (ValueError, UnicodeEncodeError, binascii.Error) as e: raise BinaryParseError(str(e), string.encode(), proto) - def to_string(self, proto, buf): + def to_string(self, proto: Any, buf: bytes) -> str: try: if len(buf) != 37: raise ValueError("Invalid onion3 address length") diff --git a/multiaddr/codecs/uint16be.py b/multiaddr/codecs/uint16be.py index de01bc3..a0c993d 100644 --- a/multiaddr/codecs/uint16be.py +++ b/multiaddr/codecs/uint16be.py @@ -1,3 +1,5 @@ +from typing import Any + from ..codecs import CodecBase SIZE = 16 @@ -8,7 +10,7 @@ class Codec(CodecBase): SIZE = SIZE IS_PATH = IS_PATH - def to_bytes(self, proto, string): + def to_bytes(self, proto: Any, string: str) -> bytes: try: n = int(string, 10) except ValueError: @@ -17,7 +19,7 @@ def to_bytes(self, proto, string): raise ValueError("integer not in range [0, 65536)") return n.to_bytes(2, byteorder="big") - def to_string(self, proto, buf): + def to_string(self, proto: Any, buf: bytes) -> str: if len(buf) != 2: raise ValueError("buffer length must be 2 bytes") return str(int.from_bytes(buf, byteorder="big")) diff --git a/multiaddr/codecs/utf8.py b/multiaddr/codecs/utf8.py index 8ee8f2f..bf29c03 100644 --- a/multiaddr/codecs/utf8.py +++ b/multiaddr/codecs/utf8.py @@ -1,4 +1,5 @@ import urllib.parse +from typing import Any from ..exceptions import BinaryParseError from . import CodecBase @@ -8,7 +9,7 @@ class Codec(CodecBase): SIZE = 0 # Variable size IS_PATH = False # Default to False, will be set to True for ip6zone protocol - def to_bytes(self, proto, string: str) -> bytes: + def to_bytes(self, proto: Any, string: str) -> bytes: """Convert a UTF-8 string to its binary representation.""" if not string: raise ValueError("String cannot be empty") @@ -28,7 +29,7 @@ def to_bytes(self, proto, string: str) -> bytes: # Do not add varint length prefix here; the framework handles it return encoded - def to_string(self, proto, buf: bytes) -> str: + def to_string(self, proto: Any, buf: bytes) -> str: """Convert a binary UTF-8 string to its string representation.""" if not buf: raise ValueError("Buffer cannot be empty") diff --git a/multiaddr/exceptions.py b/multiaddr/exceptions.py index e49aa0e..1527eab 100644 --- a/multiaddr/exceptions.py +++ b/multiaddr/exceptions.py @@ -49,7 +49,7 @@ def __init__( super().__init__(message) - def __str__(self): + def __str__(self) -> str: base = super().__str__() if self.protocol is not None: base += f" (protocol: {self.protocol})" @@ -81,12 +81,12 @@ def __init__( super().__init__(message) - def __str__(self): + def __str__(self) -> str: base = super().__str__() if self.protocol is not None: base += f" (protocol: {self.protocol})" if self.binary is not None: - base += f" (binary: {self.binary})" + base += f" (binary: {self.binary!r})" if self.original is not None: base += f" (cause: {self.original})" return base diff --git a/multiaddr/multiaddr.py b/multiaddr/multiaddr.py index a832495..aa43551 100644 --- a/multiaddr/multiaddr.py +++ b/multiaddr/multiaddr.py @@ -326,7 +326,7 @@ async def resolve(self) -> list["Multiaddr"]: """ from .resolvers.dns import DNSResolver - resolver = DNSResolver() + resolver: DNSResolver = DNSResolver() return await resolver.resolve(self) def _from_string(self, addr: str) -> None: @@ -357,8 +357,8 @@ def _from_string(self, addr: str) -> None: if part == "unix": try: # Get the next part as the path value - value = next(parts) - if not value: + unix_path_value = next(parts) + if not unix_path_value: raise exceptions.StringParseError("empty unix path", addr) # Join any remaining parts as part of the path @@ -373,7 +373,7 @@ def _from_string(self, addr: str) -> None: break if remaining_parts: - value = value + "/" + "/".join(remaining_parts) + unix_path_value = unix_path_value + "/" + "/".join(remaining_parts) proto = protocol_with_name("unix") codec = codec_by_name(proto.codec) @@ -382,7 +382,7 @@ def _from_string(self, addr: str) -> None: try: self._bytes += varint.encode(proto.code) - buf = codec.to_bytes(proto, value) + buf = codec.to_bytes(proto, unix_path_value) # Add length prefix for variable-sized or zero-sized codecs if codec.SIZE <= 0: self._bytes += varint.encode(len(buf)) @@ -396,11 +396,11 @@ def _from_string(self, addr: str) -> None: # Handle other protocols # Split protocol name and value if present + protocol_value: str | None = None if "=" in part: - proto_name, value = part.split("=", 1) + proto_name, protocol_value = part.split("=", 1) else: proto_name = part - value = None try: proto = protocol_with_name(proto_name) @@ -409,23 +409,26 @@ def _from_string(self, addr: str) -> None: # If the protocol expects a value, get it if proto.codec is not None: - if value is None: + if protocol_value is None: try: - value = next(parts) + protocol_value = next(parts) except StopIteration: raise exceptions.StringParseError( f"missing value for protocol: {proto_name}", addr ) # Validate value (optional: could add more checks here) # If value looks like a protocol name, that's an error - try: - protocol_with_name(value) - # If no exception, value is a protocol name, which is not allowed here - raise exceptions.StringParseError( - f"expected value for protocol {proto_name}, got protocol name {value}", addr - ) - except exceptions.ProtocolNotFoundError: - pass # value is not a protocol name, so it's valid as a value + if protocol_value is not None: + try: + protocol_with_name(protocol_value) + # If no exception, value is a protocol name, which is not allowed here + raise exceptions.StringParseError( + f"expected value for protocol {proto_name}, " + f"got protocol name {protocol_value}", + addr, + ) + except exceptions.ProtocolNotFoundError: + pass # value is not a protocol name, so it's valid as a value codec = codec_by_name(proto.codec) if not codec: @@ -439,7 +442,7 @@ def _from_string(self, addr: str) -> None: if proto.codec is None: continue - buf = codec.to_bytes(proto, value or "") + buf = codec.to_bytes(proto, protocol_value or "") if codec.SIZE <= 0: # Add length prefix for variable-sized or zero-sized codecs self._bytes += varint.encode(len(buf)) if buf: # Only append buffer if it's not empty diff --git a/multiaddr/resolvers/dns.py b/multiaddr/resolvers/dns.py index f911718..07a4f16 100644 --- a/multiaddr/resolvers/dns.py +++ b/multiaddr/resolvers/dns.py @@ -21,7 +21,7 @@ import logging import re -from typing import cast +from typing import Any, cast import dns.asyncresolver import dns.rdataclass @@ -49,11 +49,13 @@ class DNSResolver(Resolver): MAX_RECURSIVE_DEPTH = 32 DEFAULT_TIMEOUT = 5.0 # 5 seconds timeout - def __init__(self): + def __init__(self) -> None: """Initialize the DNS resolver.""" self._resolver = dns.asyncresolver.Resolver() - async def resolve(self, maddr: "Multiaddr", options: dict | None = None) -> list["Multiaddr"]: + async def resolve( + self, maddr: "Multiaddr", options: dict[str, Any] | None = None + ) -> list["Multiaddr"]: """ Resolve a DNS multiaddr to its actual addresses. @@ -178,7 +180,7 @@ async def _resolve_dnsaddr( with trio.CancelScope() as cancel_scope: # type: ignore[call-arg] # Set a timeout for DNS resolution cancel_scope.deadline = trio.current_time() + self.DEFAULT_TIMEOUT - cancel_scope.cancelled_caught = True + cancel_scope.cancelled_caught = True # type: ignore[misc] return await self._query_dnsaddr_txt_records( dnsaddr_hostname, peer_id, max_depth, cancel_scope diff --git a/multiaddr/transforms.py b/multiaddr/transforms.py index 47751eb..563eecc 100644 --- a/multiaddr/transforms.py +++ b/multiaddr/transforms.py @@ -41,7 +41,7 @@ def string_to_bytes(string: str) -> bytes: try: logger.debug(f"[DEBUG string_to_bytes] Raw CID value before encoding: {value}") buf = codec.to_bytes(proto, value) - logger.debug(f"[DEBUG string_to_bytes] Generated buf: proto={proto.name}, buf={buf}") + logger.debug(f"[DEBUG string_to_bytes] Generated buf: proto={proto.name}, buf={buf!r}") except Exception as exc: logger.debug(f"[DEBUG string_to_bytes] Error: {exc}") raise exceptions.StringParseError(str(exc), string) from exc @@ -93,11 +93,11 @@ def bytes_to_string(buf: bytes) -> str: value = codec.to_string(proto, bs.read(size)) logger.debug(f"[DEBUG] bytes_to_string: proto={proto.name}, value='{value}'") if codec.IS_PATH and value.startswith("/"): - strings.append(f"/{proto.name}{value}") + strings.append("/" + proto.name + value) # type: ignore[arg-type] else: - strings.append(f"/{proto.name}/{value}") + strings.append("/" + proto.name + "/" + value) # type: ignore[arg-type] else: - strings.append(f"/{proto.name}") + strings.append("/" + proto.name) # type: ignore[arg-type] except Exception as exc: # Use the code as the protocol identifier if proto is not available # Ensure we always have either a string or an integer diff --git a/pyproject.toml b/pyproject.toml index 3434ef3..cc202ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "multiaddr" -version = "0.0.9" +version = "0.0.10" description = "Python implementation of jbenet's multiaddr" readme = "README.rst" authors = [{ name = "Steven Buss", email = "steven.buss@gmail.com" }] @@ -33,7 +33,7 @@ dependencies = [ "idna", "netaddr", "psutil", - "py-cid @ git+https://github.com/ipld/py-cid.git@6c5d62f865e380116689d11348a4be82fe4f2703", + "py-cid >= 0.3.1", "py-multicodec >= 0.2.0", "trio-typing>=0.0.4", "trio>=0.26.0", @@ -42,7 +42,7 @@ dependencies = [ [project.urls] Homepage = "https://github.com/multiformats/py-multiaddr" -Download = "https://github.com/multiformats/py-multiaddr/tarball/0.0.9" +Download = "https://github.com/multiformats/py-multiaddr/tarball/0.0.10" [project.optional-dependencies] dev = [ @@ -50,7 +50,9 @@ dev = [ "build>=0.9.0", "bump_my_version>=1.2.0", "coverage>=6.5.0", + "mypy", "pre-commit", + "pyrefly", "pyright", "pytest", "pytest-cov", @@ -60,6 +62,8 @@ dev = [ "towncrier>=24,<25", "tox>=4.10.0", "twine", + "types-netaddr", + "types-psutil", "watchdog>=3.0.0", "wheel>=0.31.0", ] @@ -131,7 +135,7 @@ name = "Removals" showcontent = true [tool.bumpversion] -current_version = "0.0.9" +current_version = "0.0.10" parse = """ (?P\\d+) \\.(?P\\d+) @@ -173,12 +177,30 @@ filename = "pyproject.toml" search = "version = \"{current_version}\"" replace = "version = \"{new_version}\"" -[[tool.bumpversion.files]] -filename = "pyproject.toml" -search = "current_version = \"{current_version}\"" -replace = "current_version = \"{new_version}\"" - [[tool.bumpversion.files]] filename = "multiaddr/__init__.py" search = "__version__ = \"{current_version}\"" replace = "__version__ = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = "Download = \"https://github.com/multiformats/py-multiaddr/tarball/{current_version}\"" +replace = "Download = \"https://github.com/multiformats/py-multiaddr/tarball/{new_version}\"" + +[tool.mypy] +python_version = "3.10" +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_subclassing_any = false +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +ignore_missing_imports = true +incremental = false +strict_equality = true +strict_optional = true +warn_redundant_casts = true +warn_return_any = false +warn_unused_configs = true +warn_unused_ignores = false diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index de9bbee..632ad0a 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -201,7 +201,7 @@ async def test_resolve_cancellation_with_error(): """Test that DNS resolution can be cancelled.""" ma = Multiaddr("/dnsaddr/nonexistent.example.com") signal = trio.CancelScope() # type: ignore[call-arg] - signal.cancelled_caught = True + signal.cancelled_caught = True # type: ignore[misc] dns_resolver = DNSResolver() # Mock the DNS resolver to simulate a slow lookup that can be cancelled