pathapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. pathapi/AGENTS.md +2355 -0
  2. pathapi/README.md +205 -0
  3. pathapi/__init__.py +62 -0
  4. pathapi/_commit.py +116 -0
  5. pathapi/_discard.py +92 -0
  6. pathapi/_errorpolicy.py +106 -0
  7. pathapi/_filestat.py +194 -0
  8. pathapi/_lru.py +138 -0
  9. pathapi/_names.py +70 -0
  10. pathapi/_permissions.py +102 -0
  11. pathapi/_stubs.py +76 -0
  12. pathapi/_tree.py +136 -0
  13. pathapi/archive/__init__.py +44 -0
  14. pathapi/archive/_backend.py +393 -0
  15. pathapi/archive/_names.py +63 -0
  16. pathapi/archive/_pack.py +394 -0
  17. pathapi/archive/_path.py +352 -0
  18. pathapi/archive/_snapshot.py +269 -0
  19. pathapi/archive/_tar.py +126 -0
  20. pathapi/archive/_write.py +359 -0
  21. pathapi/archive/_zip.py +563 -0
  22. pathapi/archive/_zipentries.py +272 -0
  23. pathapi/checksum.py +61 -0
  24. pathapi/exceptions.py +22 -0
  25. pathapi/fspath.py +513 -0
  26. pathapi/ftp/__init__.py +27 -0
  27. pathapi/ftp/_backend.py +418 -0
  28. pathapi/ftp/_facts.py +117 -0
  29. pathapi/ftp/_path.py +582 -0
  30. pathapi/glob/__init__.py +27 -0
  31. pathapi/glob/_entries.py +103 -0
  32. pathapi/glob/_patterns.py +164 -0
  33. pathapi/glob/_recursion.py +180 -0
  34. pathapi/glob/_selection.py +303 -0
  35. pathapi/http/__init__.py +84 -0
  36. pathapi/http/_body.py +156 -0
  37. pathapi/http/_builtin.py +553 -0
  38. pathapi/http/_client.py +211 -0
  39. pathapi/http/_errors.py +79 -0
  40. pathapi/http/_follow.py +230 -0
  41. pathapi/http/_headers.py +233 -0
  42. pathapi/http/_pool.py +169 -0
  43. pathapi/http/_proxies.py +142 -0
  44. pathapi/http/_response.py +255 -0
  45. pathapi/http/_settings.py +12 -0
  46. pathapi/http/_status.py +68 -0
  47. pathapi/http/_transports.py +173 -0
  48. pathapi/http/testing/__init__.py +80 -0
  49. pathapi/http/testing/__main__.py +10 -0
  50. pathapi/http/testing/suite/conftest.py +98 -0
  51. pathapi/http/testing/suite/test_client_default.py +285 -0
  52. pathapi/http/testing/suite/test_client_errors.py +181 -0
  53. pathapi/http/testing/suite/test_client_peer_text.py +86 -0
  54. pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
  55. pathapi/http/testing/suite/test_client_redirects.py +231 -0
  56. pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
  57. pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
  58. pathapi/http/testing/suite/test_transport_body.py +302 -0
  59. pathapi/http/testing/suite/test_transport_send.py +662 -0
  60. pathapi/http/testing/suite/transport_fakes.py +133 -0
  61. pathapi/http/testing/suite/transport_guard.py +174 -0
  62. pathapi/http/testing/suite/transport_wire.py +511 -0
  63. pathapi/mempath.py +438 -0
  64. pathapi/path/__init__.py +19 -0
  65. pathapi/path/_info.py +10 -0
  66. pathapi/path/_path.py +198 -0
  67. pathapi/path/_pure.py +522 -0
  68. pathapi/path/_relation.py +10 -0
  69. pathapi/path/_transfer.py +10 -0
  70. pathapi/protocols/__init__.py +35 -0
  71. pathapi/protocols/_bundles.py +22 -0
  72. pathapi/protocols/_errors.py +12 -0
  73. pathapi/protocols/_info.py +65 -0
  74. pathapi/protocols/_relation.py +191 -0
  75. pathapi/protocols/_removal.py +69 -0
  76. pathapi/protocols/_support.py +122 -0
  77. pathapi/protocols/_transfer.py +100 -0
  78. pathapi/protocols/checksum.py +80 -0
  79. pathapi/protocols/fs.py +429 -0
  80. pathapi/protocols/io.py +224 -0
  81. pathapi/protocols/links.py +135 -0
  82. pathapi/protocols/pure.py +31 -0
  83. pathapi/protocols/resolve.py +97 -0
  84. pathapi/protocols/transfer.py +656 -0
  85. pathapi/protocols/tree.py +492 -0
  86. pathapi/providers.py +48 -0
  87. pathapi/py.typed +1 -0
  88. pathapi/sync/__init__.py +8 -0
  89. pathapi/sync/_compare.py +157 -0
  90. pathapi/sync/_events.py +97 -0
  91. pathapi/sync/_links.py +93 -0
  92. pathapi/sync/_offer.py +33 -0
  93. pathapi/sync/_paths.py +50 -0
  94. pathapi/sync/_replace.py +98 -0
  95. pathapi/sync/_syncer.py +989 -0
  96. pathapi/testing/__init__.py +96 -0
  97. pathapi/testing/_contract.py +522 -0
  98. pathapi/testing/_fixtures.py +83 -0
  99. pathapi/testing/_read.py +279 -0
  100. pathapi-0.1.0.dist-info/METADATA +242 -0
  101. pathapi-0.1.0.dist-info/RECORD +104 -0
  102. pathapi-0.1.0.dist-info/WHEEL +4 -0
  103. pathapi-0.1.0.dist-info/entry_points.txt +2 -0
  104. pathapi-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,133 @@
1
+ """Stand-ins for an HTTP transport, for tests that need no server: a
2
+ table-driven fake and a wrapper that records what a real one sends."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import typing
8
+
9
+ from pathapi.http import HTTPResponse, get_transport
10
+
11
+ _REASONS = {
12
+ 200: "OK",
13
+ 201: "Created",
14
+ 204: "No Content",
15
+ 207: "Multi-Status",
16
+ 301: "Moved Permanently",
17
+ 302: "Found",
18
+ 307: "Temporary Redirect",
19
+ 404: "Not Found",
20
+ 409: "Conflict",
21
+ 500: "Internal Server Error",
22
+ 502: "Bad Gateway",
23
+ }
24
+
25
+
26
+ def current_kind():
27
+ """The transport kind the test runs with: what `http_transport_kind` set."""
28
+ return os.environ.get("PATHAPI_HTTP_TRANSPORT", "builtin")
29
+
30
+
31
+ def is_client_error(error):
32
+ """Whether `error` comes from a client library (`requests`, `urllib3`)
33
+ rather than being a builtin or pathlib exception."""
34
+ return type(error).__module__.split(".")[0] in ("requests", "urllib3")
35
+
36
+
37
+ def make_transport(kind, **arguments):
38
+ """A real transport of the kind `get_transport()` names."""
39
+ return get_transport(kind)(**arguments)
40
+
41
+
42
+ def spy_on_sends(monkeypatch, project=lambda method, url, **kwargs: (method, url)):
43
+ """A list that gains `project(method, url, **keywords)` for every request
44
+ the real transport of the kind under test sends; the requests still go to
45
+ the server."""
46
+ transport_class = get_transport(current_kind())
47
+ log = []
48
+
49
+ def spy(self, method, url, *args, _original=transport_class.send, **kwargs):
50
+ log.append(project(method, url, **kwargs))
51
+ return _original(self, method, url, *args, **kwargs)
52
+
53
+ monkeypatch.setattr(transport_class, "send", spy)
54
+ return log
55
+
56
+
57
+ class Sent(typing.NamedTuple):
58
+ """One call of `send()`."""
59
+
60
+ method: str
61
+ url: str
62
+ headers: dict
63
+ body: object
64
+ timeout: object
65
+ stream: bool
66
+
67
+
68
+ def reply(status=200, content=b"", headers=None, reason=None, url=""):
69
+ """An `HTTPResponse` of `status` holding `content`."""
70
+ if reason is None:
71
+ reason = _REASONS.get(status, "")
72
+ return HTTPResponse(status, reason, headers or {}, url, content)
73
+
74
+
75
+ class FakeTransport:
76
+ """Answers from `responses`, keyed `(method, url)`, or `(method, url,
77
+ depth)` for a PROPFIND, and records every call in `sent`. A value is an
78
+ `HTTPResponse` or a callable returning one; a call with no entry goes to
79
+ `handler(method, url, headers=, body=)` when given, else gets `default`
80
+ (a 404 unless given). `fail` is raised by the next call, if set."""
81
+
82
+ def __init__(self, responses=None, default=None, handler=None):
83
+ self.responses = dict(responses or {})
84
+ self.default = default
85
+ self.handler = handler
86
+ self.sent: "list[Sent]" = []
87
+ self.closed = 0
88
+ self.fail: "BaseException | None" = None
89
+
90
+ def send(self, method, url, *, headers, body=None, timeout, stream=False):
91
+ self.sent.append(Sent(method, url, dict(headers), body, timeout, stream))
92
+ if self.fail is not None:
93
+ error, self.fail = self.fail, None
94
+ raise error
95
+ key = (method, url)
96
+ if method == "PROPFIND":
97
+ key = (method, url, headers.get("Depth"))
98
+ found = self.responses.get(key, self.responses.get((method, url)))
99
+ if found is None and self.handler is not None:
100
+ found = self.handler(method, url, headers=headers, body=body)
101
+ if found is None:
102
+ found = self.default if self.default is not None else reply(404)
103
+ response = found() if callable(found) else found
104
+ if not response.url:
105
+ response.url = url
106
+ return response
107
+
108
+ def close(self):
109
+ self.closed += 1
110
+
111
+
112
+ class RecordingTransport:
113
+ """Passes every call to `inner` and records it in `sent`."""
114
+
115
+ def __init__(self, inner):
116
+ self.inner = inner
117
+ self.sent: "list[Sent]" = []
118
+
119
+ def send(self, method, url, *, headers, body=None, timeout, stream=False):
120
+ self.sent.append(Sent(method, url, dict(headers), body, timeout, stream))
121
+ return self.inner.send(
122
+ method, url, headers=headers, body=body, timeout=timeout, stream=stream
123
+ )
124
+
125
+ def close(self):
126
+ self.inner.close()
127
+
128
+ @property
129
+ def methods(self):
130
+ return [call.method for call in self.sent]
131
+
132
+ def clear(self):
133
+ self.sent.clear()
@@ -0,0 +1,174 @@
1
+ """What keeps the transport suite on this machine and independent of it.
2
+
3
+ `install_network_guard` refuses every route off the loopback interface that
4
+ Python code can take, and `isolate_environment` removes the proxy and
5
+ certificate settings a developer's shell carries. `conftest.py` applies them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ipaddress
11
+ import os
12
+ import socket
13
+ import sys
14
+
15
+ #: Variables that are not the session's own: anything ending in `_PROXY`, and
16
+ #: the single names below.
17
+ AMBIENT_NAMES = ("NETRC", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE")
18
+
19
+ #: What a proxy-aware client is told never to proxy. A non-empty setting also
20
+ #: keeps `urllib` from reading the Windows registry or the macOS system
21
+ #: configuration for a proxy.
22
+ LOOPBACK_NO_PROXY = "127.0.0.1,localhost,::1"
23
+
24
+
25
+ class NetworkAccessBlocked(RuntimeError):
26
+ """A test tried to reach a non-loopback host. Not an OSError on purpose:
27
+ code that treats a network failure as "offline" must not swallow it."""
28
+
29
+
30
+ def is_loopback_host(host) -> bool:
31
+ if host is None:
32
+ return True
33
+ if isinstance(host, bytes):
34
+ host = host.decode("ascii", "replace")
35
+ host = str(host).strip("[]").split("%", 1)[0]
36
+ if host.lower() in ("", "localhost"):
37
+ return True
38
+ try:
39
+ return ipaddress.ip_address(host).is_loopback
40
+ except ValueError:
41
+ return False
42
+
43
+
44
+ def _address_host(address):
45
+ # AF_INET/AF_INET6 tuples carry the host first; AF_UNIX paths are local.
46
+ if isinstance(address, tuple) and address:
47
+ return address[0]
48
+ return None
49
+
50
+
51
+ def install_network_guard(monkeypatch, blocked):
52
+ """Replace, for the rest of the test, every call that would send a packet
53
+ off the loopback interface or resolve a name: it raises
54
+ `NetworkAccessBlocked` before the original is reached, and appends a
55
+ line to `blocked` so the test fails at teardown even if the error was
56
+ swallowed."""
57
+
58
+ def check(host, what):
59
+ if not is_loopback_host(host):
60
+ blocked.append(f"{what} {host!r}")
61
+ raise NetworkAccessBlocked(
62
+ f"{what} to non-loopback host {host!r}; the transport suite "
63
+ "only talks to the loopback interface"
64
+ )
65
+
66
+ def wrap_method(owner, name, what, address_of):
67
+ original = getattr(owner, name)
68
+
69
+ def guarded(self, *args, **kwargs):
70
+ check(_address_host(address_of(args, kwargs)), what)
71
+ return original(self, *args, **kwargs)
72
+
73
+ guarded.__name__ = name
74
+ monkeypatch.setattr(owner, name, guarded)
75
+
76
+ def wrap_function(owner, name, what, host_of):
77
+ original = getattr(owner, name)
78
+
79
+ def guarded(*args, **kwargs):
80
+ check(host_of(args, kwargs), what)
81
+ return original(*args, **kwargs)
82
+
83
+ guarded.__name__ = name
84
+ monkeypatch.setattr(owner, name, guarded)
85
+
86
+ def first(args, kwargs):
87
+ return args[0] if args else None
88
+
89
+ def sockaddr(args, kwargs):
90
+ return _address_host(args[0] if args else None)
91
+
92
+ wrap_method(socket.socket, "connect", "connect", lambda a, k: first(a, k))
93
+ wrap_method(socket.socket, "connect_ex", "connect", lambda a, k: first(a, k))
94
+ wrap_method(
95
+ socket.socket,
96
+ "sendto",
97
+ "sendto",
98
+ lambda a, k: a[-1] if a else None,
99
+ )
100
+ if hasattr(socket.socket, "sendmsg"): # absent on Windows
101
+ wrap_method(
102
+ socket.socket,
103
+ "sendmsg",
104
+ "sendto",
105
+ lambda a, k: a[3] if len(a) > 3 else k.get("address"),
106
+ )
107
+ wrap_function(socket, "getaddrinfo", "getaddrinfo", first)
108
+ wrap_function(socket, "gethostbyname", "gethostbyname", first)
109
+ wrap_function(socket, "gethostbyname_ex", "gethostbyname", first)
110
+ wrap_function(socket, "gethostbyaddr", "gethostbyaddr", first)
111
+ wrap_function(socket, "getnameinfo", "getnameinfo", sockaddr)
112
+
113
+ if sys.platform == "win32":
114
+ # The default Windows loop connects and sends through overlapped I/O,
115
+ # which never calls the socket methods above.
116
+ try:
117
+ import asyncio.windows_events as windows_events
118
+ except ImportError: # pragma: no cover - an unusual build
119
+ return
120
+ proactor = windows_events.IocpProactor
121
+ wrap_method(
122
+ proactor, "connect", "connect", lambda a, k: a[1] if len(a) > 1 else None
123
+ )
124
+ wrap_method(
125
+ proactor,
126
+ "sendto",
127
+ "sendto",
128
+ lambda a, k: a[3] if len(a) > 3 else k.get("addr"),
129
+ )
130
+
131
+
132
+ def is_ambient_variable(name: str) -> bool:
133
+ upper = name.upper()
134
+ return upper.endswith("_PROXY") or upper in AMBIENT_NAMES
135
+
136
+
137
+ def isolate_environment(patch, home) -> None:
138
+ """Make the process environment the session's own: no proxy, `.netrc` or
139
+ CA-bundle setting of the developer's, and a home directory (`home`, a path)
140
+ that holds nothing, so `~/.netrc` is not there to be read. `patch` is a
141
+ `MonkeyPatch`, which restores everything when it is undone."""
142
+ home = os.fspath(home)
143
+ for name in list(os.environ):
144
+ if is_ambient_variable(name):
145
+ patch.delenv(name)
146
+ patch.setenv("NO_PROXY", LOOPBACK_NO_PROXY)
147
+ patch.setenv("no_proxy", LOOPBACK_NO_PROXY)
148
+ patch.setenv("HOME", home)
149
+ patch.setenv("USERPROFILE", home)
150
+
151
+
152
+ #: Set and cleared by pytest itself around every phase of a test.
153
+ _PYTEST_OWN = ("PYTEST_CURRENT_TEST",)
154
+
155
+
156
+ def environment_snapshot() -> dict:
157
+ return {k: v for k, v in os.environ.items() if k not in _PYTEST_OWN}
158
+
159
+
160
+ def changed_variables(before: dict, after: dict) -> dict:
161
+ """`{name: (old, new)}` for every variable that differs; `None` stands
162
+ for absent."""
163
+ return {
164
+ name: (before.get(name), after.get(name))
165
+ for name in sorted(set(before) | set(after))
166
+ if before.get(name) != after.get(name)
167
+ }
168
+
169
+
170
+ def restore_environment(snapshot: dict) -> None:
171
+ for name in list(os.environ):
172
+ if name not in snapshot and name not in _PYTEST_OWN:
173
+ del os.environ[name]
174
+ os.environ.update(snapshot)