forked from Rust-related/RustPython
Compare commits
33 Commits
0.4.0
...
redox-rele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e534b10722 | ||
|
|
0785cc5aa9 | ||
|
|
eeb719e8f7 | ||
|
|
7933edad43 | ||
|
|
5f75728ede | ||
|
|
8cff0ed6c2 | ||
|
|
a8964f4108 | ||
|
|
740aeedca3 | ||
|
|
8152e7e62c | ||
|
|
b36c95b91e | ||
|
|
b5c1fd95dc | ||
|
|
23f7cbf1c3 | ||
|
|
ae78ecc2c5 | ||
|
|
dd06516d1c | ||
|
|
8066f36a4e | ||
|
|
3bbf6b9d53 | ||
|
|
8bc5944178 | ||
|
|
a13b99642b | ||
|
|
060db5983c | ||
|
|
42bba6920e | ||
|
|
ea11d78995 | ||
|
|
fe63ca762f | ||
|
|
a82982725e | ||
|
|
7dfb760421 | ||
|
|
b6e9a3f37e | ||
|
|
3f28309b7b | ||
|
|
d2a4a330f9 | ||
|
|
d8c35770ab | ||
|
|
dbb6794a41 | ||
|
|
63c9909aa0 | ||
|
|
f1dac5087e | ||
|
|
4f80d7013e | ||
|
|
2919df1df5 |
17
.github/workflows/ci.yaml
vendored
17
.github/workflows/ci.yaml
vendored
@@ -15,7 +15,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,ssl
|
||||
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,sqlite,ssl
|
||||
# Skip additional tests on Windows. They are checked on Linux and MacOS.
|
||||
WINDOWS_SKIPS: >-
|
||||
test_datetime
|
||||
@@ -216,13 +216,6 @@ jobs:
|
||||
- name: Check compilation for freebsd
|
||||
run: cargo check --target x86_64-unknown-freebsd
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: Check compilation for wasm32
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-unknown-freebsd
|
||||
@@ -380,6 +373,14 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/demo
|
||||
- uses: mwilliamson/setup-wabt-action@v1
|
||||
with: { wabt-version: "1.0.30" }
|
||||
- name: check wasm32-unknown without js
|
||||
run: |
|
||||
cargo build --release --manifest-path wasm/wasm-unknown-test/Cargo.toml --target wasm32-unknown-unknown --verbose
|
||||
if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then
|
||||
echo "ERROR: wasm32-unknown module expects imports from the host environment" >2
|
||||
fi
|
||||
- name: build notebook demo
|
||||
if: github.ref == 'refs/heads/release'
|
||||
run: |
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2273,6 +2273,7 @@ dependencies = [
|
||||
"puruspe",
|
||||
"rand",
|
||||
"rand_core",
|
||||
"rustix",
|
||||
"rustpython-common",
|
||||
"rustpython-derive",
|
||||
"rustpython-vm",
|
||||
@@ -2342,6 +2343,7 @@ dependencies = [
|
||||
"rand",
|
||||
"result-like",
|
||||
"rustc_version",
|
||||
"rustix",
|
||||
"rustpython-ast",
|
||||
"rustpython-codegen",
|
||||
"rustpython-common",
|
||||
|
||||
183
Cargo.toml
183
Cargo.toml
@@ -1,92 +1,13 @@
|
||||
[package]
|
||||
name = "rustpython"
|
||||
version = "0.4.0"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2021"
|
||||
rust-version = "1.75.0"
|
||||
description = "A python interpreter written in rust."
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
include = ["LICENSE", "Cargo.toml", "src/**/*.rs"]
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"compiler", "compiler/core", "compiler/codegen",
|
||||
".", "common", "derive", "jit", "vm", "vm/sre_engine", "pylib", "stdlib", "wasm/lib", "derive-impl",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
rustpython-compiler-core = { path = "compiler/core", version = "0.4.0" }
|
||||
rustpython-compiler = { path = "compiler", version = "0.4.0" }
|
||||
rustpython-codegen = { path = "compiler/codegen", version = "0.4.0" }
|
||||
rustpython-common = { path = "common", version = "0.4.0" }
|
||||
rustpython-derive = { path = "derive", version = "0.4.0" }
|
||||
rustpython-derive-impl = { path = "derive-impl", version = "0.4.0" }
|
||||
rustpython-jit = { path = "jit", version = "0.4.0" }
|
||||
rustpython-vm = { path = "vm", default-features = false, version = "0.4.0" }
|
||||
rustpython-pylib = { path = "pylib", version = "0.4.0" }
|
||||
rustpython-stdlib = { path = "stdlib", default-features = false, version = "0.4.0" }
|
||||
rustpython-sre_engine = { path = "vm/sre_engine", version = "0.4.0" }
|
||||
rustpython-doc = { git = "https://github.com/RustPython/__doc__", tag = "0.3.0", version = "0.3.0" }
|
||||
|
||||
rustpython-literal = { version = "0.4.0" }
|
||||
rustpython-parser-core = { version = "0.4.0" }
|
||||
rustpython-parser = { version = "0.4.0" }
|
||||
rustpython-ast = { version = "0.4.0" }
|
||||
rustpython-format= { version = "0.4.0" }
|
||||
# rustpython-literal = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-parser-core = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-parser = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-ast = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-format = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-literal = { path = "../RustPython-parser/literal" }
|
||||
# rustpython-parser-core = { path = "../RustPython-parser/core" }
|
||||
# rustpython-parser = { path = "../RustPython-parser/parser" }
|
||||
# rustpython-ast = { path = "../RustPython-parser/ast" }
|
||||
# rustpython-format = { path = "../RustPython-parser/format" }
|
||||
|
||||
ahash = "0.8.11"
|
||||
ascii = "1.0"
|
||||
atty = "0.2.14"
|
||||
bitflags = "2.4.1"
|
||||
bstr = "0.2.17"
|
||||
cfg-if = "1.0"
|
||||
chrono = "0.4.37"
|
||||
crossbeam-utils = "0.8.19"
|
||||
flame = "0.2.2"
|
||||
glob = "0.3"
|
||||
hex = "0.4.3"
|
||||
indexmap = { version = "2.2.6", features = ["std"] }
|
||||
insta = "1.38.0"
|
||||
itertools = "0.11.0"
|
||||
is-macro = "0.3.0"
|
||||
junction = "1.0.0"
|
||||
libc = "0.2.153"
|
||||
log = "0.4.16"
|
||||
nix = { version = "0.27", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] }
|
||||
malachite-bigint = "0.2.0"
|
||||
malachite-q = "0.4.4"
|
||||
malachite-base = "0.4.4"
|
||||
memchr = "2.7.2"
|
||||
num-complex = "0.4.0"
|
||||
num-integer = "0.1.44"
|
||||
num-traits = "0.2"
|
||||
num_enum = "0.7"
|
||||
once_cell = "1.19.0"
|
||||
parking_lot = "0.12.1"
|
||||
paste = "1.0.7"
|
||||
rand = "0.8.5"
|
||||
rustyline = "14.0.0"
|
||||
serde = { version = "1.0.133", default-features = false }
|
||||
schannel = "0.1.22"
|
||||
static_assertions = "1.1"
|
||||
syn = "1.0.109"
|
||||
thiserror = "1.0"
|
||||
thread_local = "1.1.4"
|
||||
unicode_names2 = "1.2.0"
|
||||
widestring = "1.1.0"
|
||||
windows-sys = "0.52.0"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["threading", "stdlib", "zlib", "importlib"]
|
||||
@@ -99,6 +20,7 @@ jit = ["rustpython-vm/jit"]
|
||||
threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"]
|
||||
zlib = ["stdlib", "rustpython-stdlib/zlib"]
|
||||
bz2 = ["stdlib", "rustpython-stdlib/bz2"]
|
||||
sqlite = ["rustpython-stdlib/sqlite"]
|
||||
ssl = ["rustpython-stdlib/ssl"]
|
||||
ssl-vendor = ["ssl", "rustpython-stdlib/ssl-vendor"]
|
||||
|
||||
@@ -171,6 +93,97 @@ rev = "2024.02.14"
|
||||
[package.metadata.vcpkg.target]
|
||||
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md", dev-dependencies = ["openssl" ] }
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"compiler", "compiler/core", "compiler/codegen",
|
||||
".", "common", "derive", "jit", "vm", "vm/sre_engine", "pylib", "stdlib", "derive-impl",
|
||||
"wasm/lib",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2021"
|
||||
rust-version = "1.78.0"
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
rustpython-compiler-core = { path = "compiler/core", version = "0.4.0" }
|
||||
rustpython-compiler = { path = "compiler", version = "0.4.0" }
|
||||
rustpython-codegen = { path = "compiler/codegen", version = "0.4.0" }
|
||||
rustpython-common = { path = "common", version = "0.4.0" }
|
||||
rustpython-derive = { path = "derive", version = "0.4.0" }
|
||||
rustpython-derive-impl = { path = "derive-impl", version = "0.4.0" }
|
||||
rustpython-jit = { path = "jit", version = "0.4.0" }
|
||||
rustpython-vm = { path = "vm", default-features = false, version = "0.4.0" }
|
||||
rustpython-pylib = { path = "pylib", version = "0.4.0" }
|
||||
rustpython-stdlib = { path = "stdlib", default-features = false, version = "0.4.0" }
|
||||
rustpython-sre_engine = { path = "vm/sre_engine", version = "0.4.0" }
|
||||
rustpython-doc = { git = "https://github.com/RustPython/__doc__", tag = "0.3.0", version = "0.3.0" }
|
||||
|
||||
rustpython-literal = { version = "0.4.0" }
|
||||
rustpython-parser-core = { version = "0.4.0" }
|
||||
rustpython-parser = { version = "0.4.0" }
|
||||
rustpython-ast = { version = "0.4.0" }
|
||||
rustpython-format= { version = "0.4.0" }
|
||||
# rustpython-literal = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-parser-core = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-parser = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-ast = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-format = { git = "https://github.com/RustPython/Parser.git", version = "0.4.0", rev = "00d2f1d1a7522ef9c85c10dfa5f0bb7178dee655" }
|
||||
# rustpython-literal = { path = "../RustPython-parser/literal" }
|
||||
# rustpython-parser-core = { path = "../RustPython-parser/core" }
|
||||
# rustpython-parser = { path = "../RustPython-parser/parser" }
|
||||
# rustpython-ast = { path = "../RustPython-parser/ast" }
|
||||
# rustpython-format = { path = "../RustPython-parser/format" }
|
||||
|
||||
ahash = "0.8.11"
|
||||
ascii = "1.0"
|
||||
atty = "0.2.14"
|
||||
bitflags = "2.4.1"
|
||||
bstr = "0.2.17"
|
||||
cfg-if = "1.0"
|
||||
chrono = "0.4.37"
|
||||
crossbeam-utils = "0.8.19"
|
||||
flame = "0.2.2"
|
||||
getrandom = "0.2.12"
|
||||
glob = "0.3"
|
||||
hex = "0.4.3"
|
||||
indexmap = { version = "2.2.6", features = ["std"] }
|
||||
insta = "1.38.0"
|
||||
itertools = "0.11.0"
|
||||
is-macro = "0.3.0"
|
||||
junction = "1.0.0"
|
||||
libc = "0.2.153"
|
||||
log = "0.4.16"
|
||||
nix = { version = "0.27", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] }
|
||||
malachite-bigint = "0.2.0"
|
||||
malachite-q = "0.4.4"
|
||||
malachite-base = "0.4.4"
|
||||
memchr = "2.7.2"
|
||||
num-complex = "0.4.0"
|
||||
num-integer = "0.1.44"
|
||||
num-traits = "0.2"
|
||||
num_enum = "0.7"
|
||||
once_cell = "1.19.0"
|
||||
parking_lot = "0.12.1"
|
||||
paste = "1.0.7"
|
||||
rand = "0.8.5"
|
||||
rustix = { version = "0.38", features = ["event"] }
|
||||
rustyline = "14.0.0"
|
||||
serde = { version = "1.0.133", default-features = false }
|
||||
schannel = "0.1.22"
|
||||
static_assertions = "1.1"
|
||||
syn = "1.0.109"
|
||||
thiserror = "1.0"
|
||||
thread_local = "1.1.4"
|
||||
unicode_names2 = "1.2.0"
|
||||
widestring = "1.1.0"
|
||||
windows-sys = "0.52.0"
|
||||
wasm-bindgen = "0.2.92"
|
||||
|
||||
# Lints
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
27
Lib/_pydecimal.py
vendored
27
Lib/_pydecimal.py
vendored
@@ -734,18 +734,23 @@ class Decimal(object):
|
||||
|
||||
"""
|
||||
if isinstance(f, int): # handle integer inputs
|
||||
return cls(f)
|
||||
if not isinstance(f, float):
|
||||
raise TypeError("argument must be int or float.")
|
||||
if _math.isinf(f) or _math.isnan(f):
|
||||
return cls(repr(f))
|
||||
if _math.copysign(1.0, f) == 1.0:
|
||||
sign = 0
|
||||
sign = 0 if f >= 0 else 1
|
||||
k = 0
|
||||
coeff = str(abs(f))
|
||||
elif isinstance(f, float):
|
||||
if _math.isinf(f) or _math.isnan(f):
|
||||
return cls(repr(f))
|
||||
if _math.copysign(1.0, f) == 1.0:
|
||||
sign = 0
|
||||
else:
|
||||
sign = 1
|
||||
n, d = abs(f).as_integer_ratio()
|
||||
k = d.bit_length() - 1
|
||||
coeff = str(n*5**k)
|
||||
else:
|
||||
sign = 1
|
||||
n, d = abs(f).as_integer_ratio()
|
||||
k = d.bit_length() - 1
|
||||
result = _dec_from_triple(sign, str(n*5**k), -k)
|
||||
raise TypeError("argument must be int or float.")
|
||||
|
||||
result = _dec_from_triple(sign, coeff, -k)
|
||||
if cls is Decimal:
|
||||
return result
|
||||
else:
|
||||
|
||||
111
Lib/test/support/_hypothesis_stubs/__init__.py
vendored
Normal file
111
Lib/test/support/_hypothesis_stubs/__init__.py
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
from enum import Enum
|
||||
import functools
|
||||
import unittest
|
||||
|
||||
__all__ = [
|
||||
"given",
|
||||
"example",
|
||||
"assume",
|
||||
"reject",
|
||||
"register_random",
|
||||
"strategies",
|
||||
"HealthCheck",
|
||||
"settings",
|
||||
"Verbosity",
|
||||
]
|
||||
|
||||
from . import strategies
|
||||
|
||||
|
||||
def given(*_args, **_kwargs):
|
||||
def decorator(f):
|
||||
if examples := getattr(f, "_examples", []):
|
||||
|
||||
@functools.wraps(f)
|
||||
def test_function(self):
|
||||
for example_args, example_kwargs in examples:
|
||||
with self.subTest(*example_args, **example_kwargs):
|
||||
f(self, *example_args, **example_kwargs)
|
||||
|
||||
else:
|
||||
# If we have found no examples, we must skip the test. If @example
|
||||
# is applied after @given, it will re-wrap the test to remove the
|
||||
# skip decorator.
|
||||
test_function = unittest.skip(
|
||||
"Hypothesis required for property test with no " +
|
||||
"specified examples"
|
||||
)(f)
|
||||
|
||||
test_function._given = True
|
||||
return test_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def example(*args, **kwargs):
|
||||
if bool(args) == bool(kwargs):
|
||||
raise ValueError("Must specify exactly one of *args or **kwargs")
|
||||
|
||||
def decorator(f):
|
||||
base_func = getattr(f, "__wrapped__", f)
|
||||
if not hasattr(base_func, "_examples"):
|
||||
base_func._examples = []
|
||||
|
||||
base_func._examples.append((args, kwargs))
|
||||
|
||||
if getattr(f, "_given", False):
|
||||
# If the given decorator is below all the example decorators,
|
||||
# it would be erroneously skipped, so we need to re-wrap the new
|
||||
# base function.
|
||||
f = given()(base_func)
|
||||
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def assume(condition):
|
||||
if not condition:
|
||||
raise unittest.SkipTest("Unsatisfied assumption")
|
||||
return True
|
||||
|
||||
|
||||
def reject():
|
||||
assume(False)
|
||||
|
||||
|
||||
def register_random(*args, **kwargs):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def settings(*args, **kwargs):
|
||||
return lambda f: f # pragma: nocover
|
||||
|
||||
|
||||
class HealthCheck(Enum):
|
||||
data_too_large = 1
|
||||
filter_too_much = 2
|
||||
too_slow = 3
|
||||
return_value = 5
|
||||
large_base_example = 7
|
||||
not_a_test_method = 8
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
return list(cls)
|
||||
|
||||
|
||||
class Verbosity(Enum):
|
||||
quiet = 0
|
||||
normal = 1
|
||||
verbose = 2
|
||||
debug = 3
|
||||
|
||||
|
||||
class Phase(Enum):
|
||||
explicit = 0
|
||||
reuse = 1
|
||||
generate = 2
|
||||
target = 3
|
||||
shrink = 4
|
||||
explain = 5
|
||||
43
Lib/test/support/_hypothesis_stubs/_helpers.py
vendored
Normal file
43
Lib/test/support/_hypothesis_stubs/_helpers.py
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# Stub out only the subset of the interface that we actually use in our tests.
|
||||
class StubClass:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__stub_args = args
|
||||
self.__stub_kwargs = kwargs
|
||||
self.__repr = None
|
||||
|
||||
def _with_repr(self, new_repr):
|
||||
new_obj = self.__class__(*self.__stub_args, **self.__stub_kwargs)
|
||||
new_obj.__repr = new_repr
|
||||
return new_obj
|
||||
|
||||
def __repr__(self):
|
||||
if self.__repr is not None:
|
||||
return self.__repr
|
||||
|
||||
argstr = ", ".join(self.__stub_args)
|
||||
kwargstr = ", ".join(f"{kw}={val}" for kw, val in self.__stub_kwargs.items())
|
||||
|
||||
in_parens = argstr
|
||||
if kwargstr:
|
||||
in_parens += ", " + kwargstr
|
||||
|
||||
return f"{self.__class__.__qualname__}({in_parens})"
|
||||
|
||||
|
||||
def stub_factory(klass, name, *, with_repr=None, _seen={}):
|
||||
if (klass, name) not in _seen:
|
||||
|
||||
class Stub(klass):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
self.__stub_args = args
|
||||
self.__stub_kwargs = kwargs
|
||||
|
||||
Stub.__name__ = name
|
||||
Stub.__qualname__ = name
|
||||
if with_repr is not None:
|
||||
Stub._repr = None
|
||||
|
||||
_seen.setdefault((klass, name, with_repr), Stub)
|
||||
|
||||
return _seen[(klass, name, with_repr)]
|
||||
91
Lib/test/support/_hypothesis_stubs/strategies.py
vendored
Normal file
91
Lib/test/support/_hypothesis_stubs/strategies.py
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
import functools
|
||||
|
||||
from ._helpers import StubClass, stub_factory
|
||||
|
||||
|
||||
class StubStrategy(StubClass):
|
||||
def __make_trailing_repr(self, transformation_name, func):
|
||||
func_name = func.__name__ or repr(func)
|
||||
return f"{self!r}.{transformation_name}({func_name})"
|
||||
|
||||
def map(self, pack):
|
||||
return self._with_repr(self.__make_trailing_repr("map", pack))
|
||||
|
||||
def flatmap(self, expand):
|
||||
return self._with_repr(self.__make_trailing_repr("flatmap", expand))
|
||||
|
||||
def filter(self, condition):
|
||||
return self._with_repr(self.__make_trailing_repr("filter", condition))
|
||||
|
||||
def __or__(self, other):
|
||||
new_repr = f"one_of({self!r}, {other!r})"
|
||||
return self._with_repr(new_repr)
|
||||
|
||||
|
||||
_STRATEGIES = {
|
||||
"binary",
|
||||
"booleans",
|
||||
"builds",
|
||||
"characters",
|
||||
"complex_numbers",
|
||||
"composite",
|
||||
"data",
|
||||
"dates",
|
||||
"datetimes",
|
||||
"decimals",
|
||||
"deferred",
|
||||
"dictionaries",
|
||||
"emails",
|
||||
"fixed_dictionaries",
|
||||
"floats",
|
||||
"fractions",
|
||||
"from_regex",
|
||||
"from_type",
|
||||
"frozensets",
|
||||
"functions",
|
||||
"integers",
|
||||
"iterables",
|
||||
"just",
|
||||
"lists",
|
||||
"none",
|
||||
"nothing",
|
||||
"one_of",
|
||||
"permutations",
|
||||
"random_module",
|
||||
"randoms",
|
||||
"recursive",
|
||||
"register_type_strategy",
|
||||
"runner",
|
||||
"sampled_from",
|
||||
"sets",
|
||||
"shared",
|
||||
"slices",
|
||||
"timedeltas",
|
||||
"times",
|
||||
"text",
|
||||
"tuples",
|
||||
"uuids",
|
||||
}
|
||||
|
||||
__all__ = sorted(_STRATEGIES)
|
||||
|
||||
|
||||
def composite(f):
|
||||
strategy = stub_factory(StubStrategy, f.__name__)
|
||||
|
||||
@functools.wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
return strategy(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name not in _STRATEGIES:
|
||||
raise AttributeError(f"Unknown attribute {name}")
|
||||
|
||||
return stub_factory(StubStrategy, f"hypothesis.strategies.{name}")
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
45
Lib/test/support/hypothesis_helper.py
vendored
Normal file
45
Lib/test/support/hypothesis_helper.py
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
|
||||
try:
|
||||
import hypothesis
|
||||
except ImportError:
|
||||
from . import _hypothesis_stubs as hypothesis
|
||||
else:
|
||||
# Regrtest changes to use a tempdir as the working directory, so we have
|
||||
# to tell Hypothesis to use the original in order to persist the database.
|
||||
from .os_helper import SAVEDCWD
|
||||
from hypothesis.configuration import set_hypothesis_home_dir
|
||||
|
||||
set_hypothesis_home_dir(os.path.join(SAVEDCWD, ".hypothesis"))
|
||||
|
||||
# When using the real Hypothesis, we'll configure it to ignore occasional
|
||||
# slow tests (avoiding flakiness from random VM slowness in CI).
|
||||
hypothesis.settings.register_profile(
|
||||
"slow-is-ok",
|
||||
deadline=None,
|
||||
suppress_health_check=[
|
||||
hypothesis.HealthCheck.too_slow,
|
||||
hypothesis.HealthCheck.differing_executors,
|
||||
],
|
||||
)
|
||||
hypothesis.settings.load_profile("slow-is-ok")
|
||||
|
||||
# For local development, we'll write to the default on-local-disk database
|
||||
# of failing examples, and also use a pull-through cache to automatically
|
||||
# replay any failing examples discovered in CI. For details on how this
|
||||
# works, see https://hypothesis.readthedocs.io/en/latest/database.html
|
||||
if "CI" not in os.environ:
|
||||
from hypothesis.database import (
|
||||
GitHubArtifactDatabase,
|
||||
MultiplexedDatabase,
|
||||
ReadOnlyDatabase,
|
||||
)
|
||||
|
||||
hypothesis.settings.register_profile(
|
||||
"cpython-local-dev",
|
||||
database=MultiplexedDatabase(
|
||||
hypothesis.settings.default.database,
|
||||
ReadOnlyDatabase(GitHubArtifactDatabase("python", "cpython")),
|
||||
),
|
||||
)
|
||||
hypothesis.settings.load_profile("cpython-local-dev")
|
||||
2
Lib/test/test_complex.py
vendored
2
Lib/test/test_complex.py
vendored
@@ -568,8 +568,6 @@ class ComplexTest(unittest.TestCase):
|
||||
self.assertFloatsAreIdentical(z.real, x)
|
||||
self.assertFloatsAreIdentical(z.imag, y)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_constructor_negative_nans_from_string(self):
|
||||
self.assertEqual(copysign(1., complex("-nan").real), -1.)
|
||||
self.assertEqual(copysign(1., complex("-nanj").imag), -1.)
|
||||
|
||||
4
Lib/test/test_decimal.py
vendored
4
Lib/test/test_decimal.py
vendored
@@ -1257,8 +1257,6 @@ class FormatTest:
|
||||
self.assertEqual(format(Decimal('100000000.123'), 'n'),
|
||||
'100\u066c000\u066c000\u066b123')
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_decimal_from_float_argument_type(self):
|
||||
class A(self.decimal.Decimal):
|
||||
def __init__(self, a):
|
||||
@@ -2079,8 +2077,6 @@ class UsabilityTest:
|
||||
for d, n, r in test_triples:
|
||||
self.assertEqual(str(round(Decimal(d), n)), r)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_nan_to_float(self):
|
||||
# Test conversions of decimal NANs to float.
|
||||
# See http://bugs.python.org/issue15544
|
||||
|
||||
1
Lib/test/test_exception_hierarchy.py
vendored
1
Lib/test/test_exception_hierarchy.py
vendored
@@ -81,7 +81,6 @@ class HierarchyTest(unittest.TestCase):
|
||||
return _map
|
||||
_map = _make_map(_pep_map)
|
||||
|
||||
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON")
|
||||
def test_errno_mapping(self):
|
||||
# The OSError constructor maps errnos to subclasses
|
||||
# A sample test for the basic functionality
|
||||
|
||||
2
Lib/test/test_float.py
vendored
2
Lib/test/test_float.py
vendored
@@ -1053,8 +1053,6 @@ class InfNanTest(unittest.TestCase):
|
||||
self.assertEqual(copysign(1.0, float('inf')), 1.0)
|
||||
self.assertEqual(copysign(1.0, float('-inf')), -1.0)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short',
|
||||
"applies only when using short float repr style")
|
||||
def test_nan_signs(self):
|
||||
|
||||
2
Lib/test/test_long.py
vendored
2
Lib/test/test_long.py
vendored
@@ -378,8 +378,6 @@ class LongTest(unittest.TestCase):
|
||||
self.assertRaises(ValueError, int, '\u3053\u3093\u306b\u3061\u306f')
|
||||
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_conversion(self):
|
||||
|
||||
class JustLong:
|
||||
|
||||
2
Lib/test/test_poll.py
vendored
2
Lib/test/test_poll.py
vendored
@@ -211,8 +211,6 @@ class PollTests(unittest.TestCase):
|
||||
os.write(w, b'spam')
|
||||
t.join()
|
||||
|
||||
# TODO: RUSTPYTHON add support for negative timeout
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipUnless(threading, 'Threading required for this test.')
|
||||
@threading_helper.reap_threads
|
||||
def test_poll_blocks_with_negative_ms(self):
|
||||
|
||||
2
Lib/test/test_re.py
vendored
2
Lib/test/test_re.py
vendored
@@ -2246,7 +2246,7 @@ class ReTests(unittest.TestCase):
|
||||
t = time.perf_counter() - start
|
||||
# Without optimization it takes 1 second on my computer.
|
||||
# With optimization -- 0.0003 seconds.
|
||||
self.assertLess(t, 0.1)
|
||||
self.assertLess(t, 0.2)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
|
||||
8
Lib/test/test_subprocess.py
vendored
8
Lib/test/test_subprocess.py
vendored
@@ -1614,8 +1614,6 @@ class ProcessTestCase(BaseTestCase):
|
||||
subprocess.call(['/opt/nonexistent_binary', 'with', 'some', 'args'])
|
||||
self.assertEqual(c.exception.filename, '/opt/nonexistent_binary')
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipIf(mswindows, "behavior currently not supported on Windows")
|
||||
def test_file_not_found_with_bad_cwd(self):
|
||||
with self.assertRaises(FileNotFoundError) as c:
|
||||
@@ -2033,8 +2031,6 @@ class POSIXProcessTestCase(BaseTestCase):
|
||||
child_sid = int(output)
|
||||
self.assertNotEqual(parent_sid, child_sid)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipUnless(hasattr(os, 'setpgid') and hasattr(os, 'getpgid'),
|
||||
'no setpgid or getpgid on platform')
|
||||
def test_process_group_0(self):
|
||||
@@ -2053,8 +2049,6 @@ class POSIXProcessTestCase(BaseTestCase):
|
||||
child_pgid = int(output)
|
||||
self.assertNotEqual(parent_pgid, child_pgid)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipUnless(hasattr(os, 'setreuid'), 'no setreuid on platform')
|
||||
def test_user(self):
|
||||
# For code coverage of the user parameter. We don't care if we get a
|
||||
@@ -2112,8 +2106,6 @@ class POSIXProcessTestCase(BaseTestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
subprocess.check_call(ZERO_RETURN_CMD, user=65535)
|
||||
|
||||
# TODO: RUSTPYTHON, observed gids do not match expected gids
|
||||
@unittest.expectedFailure
|
||||
@unittest.skipUnless(hasattr(os, 'setregid'), 'no setregid() on platform')
|
||||
def test_group(self):
|
||||
gid = os.getegid()
|
||||
|
||||
2
Lib/test/test_weakset.py
vendored
2
Lib/test/test_weakset.py
vendored
@@ -322,8 +322,6 @@ class TestWeakSet(unittest.TestCase):
|
||||
else:
|
||||
self.assertNotIn(c, self.s)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_inplace_on_self(self):
|
||||
t = self.s.copy()
|
||||
t |= t
|
||||
|
||||
5
Lib/test/test_zoneinfo/__init__.py
vendored
Normal file
5
Lib/test/test_zoneinfo/__init__.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
from test.support import load_package_tests
|
||||
|
||||
def load_tests(*args):
|
||||
return load_package_tests(os.path.dirname(__file__), *args)
|
||||
3
Lib/test/test_zoneinfo/__main__.py
vendored
Normal file
3
Lib/test/test_zoneinfo/__main__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import unittest
|
||||
|
||||
unittest.main('test.test_zoneinfo')
|
||||
100
Lib/test/test_zoneinfo/_support.py
vendored
Normal file
100
Lib/test/test_zoneinfo/_support.py
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
import contextlib
|
||||
import functools
|
||||
import sys
|
||||
import threading
|
||||
import unittest
|
||||
from test.support.import_helper import import_fresh_module
|
||||
|
||||
OS_ENV_LOCK = threading.Lock()
|
||||
TZPATH_LOCK = threading.Lock()
|
||||
TZPATH_TEST_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def call_once(f):
|
||||
"""Decorator that ensures a function is only ever called once."""
|
||||
lock = threading.Lock()
|
||||
cached = functools.lru_cache(None)(f)
|
||||
|
||||
@functools.wraps(f)
|
||||
def inner():
|
||||
with lock:
|
||||
return cached()
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@call_once
|
||||
def get_modules():
|
||||
"""Retrieve two copies of zoneinfo: pure Python and C accelerated.
|
||||
|
||||
Because this function manipulates the import system in a way that might
|
||||
be fragile or do unexpected things if it is run many times, it uses a
|
||||
`call_once` decorator to ensure that this is only ever called exactly
|
||||
one time — in other words, when using this function you will only ever
|
||||
get one copy of each module rather than a fresh import each time.
|
||||
"""
|
||||
import zoneinfo as c_module
|
||||
|
||||
py_module = import_fresh_module("zoneinfo", blocked=["_zoneinfo"])
|
||||
|
||||
return py_module, c_module
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def set_zoneinfo_module(module):
|
||||
"""Make sure sys.modules["zoneinfo"] refers to `module`.
|
||||
|
||||
This is necessary because `pickle` will refuse to serialize
|
||||
an type calling itself `zoneinfo.ZoneInfo` unless `zoneinfo.ZoneInfo`
|
||||
refers to the same object.
|
||||
"""
|
||||
|
||||
NOT_PRESENT = object()
|
||||
old_zoneinfo = sys.modules.get("zoneinfo", NOT_PRESENT)
|
||||
sys.modules["zoneinfo"] = module
|
||||
yield
|
||||
if old_zoneinfo is not NOT_PRESENT:
|
||||
sys.modules["zoneinfo"] = old_zoneinfo
|
||||
else: # pragma: nocover
|
||||
sys.modules.pop("zoneinfo")
|
||||
|
||||
|
||||
class ZoneInfoTestBase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.klass = cls.module.ZoneInfo
|
||||
super().setUpClass()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tzpath_context(self, tzpath, block_tzdata=True, lock=TZPATH_LOCK):
|
||||
def pop_tzdata_modules():
|
||||
tzdata_modules = {}
|
||||
for modname in list(sys.modules):
|
||||
if modname.split(".", 1)[0] != "tzdata": # pragma: nocover
|
||||
continue
|
||||
|
||||
tzdata_modules[modname] = sys.modules.pop(modname)
|
||||
|
||||
return tzdata_modules
|
||||
|
||||
with lock:
|
||||
if block_tzdata:
|
||||
# In order to fully exclude tzdata from the path, we need to
|
||||
# clear the sys.modules cache of all its contents — setting the
|
||||
# root package to None is not enough to block direct access of
|
||||
# already-imported submodules (though it will prevent new
|
||||
# imports of submodules).
|
||||
tzdata_modules = pop_tzdata_modules()
|
||||
sys.modules["tzdata"] = None
|
||||
|
||||
old_path = self.module.TZPATH
|
||||
try:
|
||||
self.module.reset_tzpath(tzpath)
|
||||
yield
|
||||
finally:
|
||||
if block_tzdata:
|
||||
sys.modules.pop("tzdata")
|
||||
for modname, module in tzdata_modules.items():
|
||||
sys.modules[modname] = module
|
||||
|
||||
self.module.reset_tzpath(old_path)
|
||||
0
Lib/test/test_zoneinfo/data/update_test_data.py
vendored
Normal file
0
Lib/test/test_zoneinfo/data/update_test_data.py
vendored
Normal file
190
Lib/test/test_zoneinfo/data/zoneinfo_data.json
vendored
Normal file
190
Lib/test/test_zoneinfo/data/zoneinfo_data.json
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"data": {
|
||||
"Africa/Abidjan": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~f{VGF<>F7KxBg5R*{Ksocg8-YYVul=v7vZzaHN",
|
||||
"uC=da5UI2rH18c!OnjV{y4u(+A!!VBKmY&$ORw>7UO^(500B;v0RR91bXh%WvBYQl0ssI2",
|
||||
"00dcD"
|
||||
],
|
||||
"Africa/Casablanca": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0b&Kz+C_;7KxBg5R*{N&yjMUR~;C-fDaSOU;q-~",
|
||||
"FqW+4{YBjbcw}`a!dW>b)R2-0a+uwf`P3{_Y@HuCz}S$J$ZJ>R_V<~|Fk>sgX4=%0vUrh-",
|
||||
"lt@YP^Wrus;j?`Th#xRPzf<<~Hp4DH^gZX>d{+WOp~HNu8!{uWu}&XphAd{j1;rB4|9?R!",
|
||||
"pqruAFUMt8#*WcrVS{;kLlY(cJRV$w?d2car%R<ALOSO?^`4;ZZtI)%f^^G^>s>q9BgTU4",
|
||||
"Ht-tQKZ7Z`9QqOb?R#b%z?rk>!CkH7jy3wja4NG2q)H}fNRKg8v{);Em;K3Cncf4C6&Oaj",
|
||||
"V+DbX%o4+)CV3+e!Lm6dutu(0BQpH1T?W(~cQtKV*^_Pdx!LirjpTs?Bmt@vktjLq4;)O!",
|
||||
"rrly=c*rwTwMJFd0I57`hgkc?=nyI4RZf9W$6DCWugmf&)wk^tWH17owj=#PGH7Xv-?9$j",
|
||||
"njwDlkOE+BFNR9YXEmBpO;rqEw=e2IR-8^(W;8ma?M3JVd($2T>IW+0tk|Gm8>ftukRQ9J",
|
||||
"8k3brzqMnVyjsLI-CKneFa)Lxvp_a<CkQEd#(pMA^rr}rBNElGA=*!M)puBdoErR9{kWL@",
|
||||
"w=svMc6eZ^-(vQZrV<u^PY#nOIUDJ8%A&;BUVlY9=;@i2j2J1_`P>q40f}0J3VVoWL5rox",
|
||||
"`Kptivcp}o5xA^@>qNI%?zo=Yj4AMV?kbAA)j(1%)+Pp)bSn+7Yk`M{oE}L-Z!G6<Dgq&*",
|
||||
"(C-mFJfbEGDH5M^vBr65rcnsx*~|Em_GeU#B)(+T!|MG-nxj0@IPbp-nHejH3~>OMr5G+h",
|
||||
"p)$3Lg{ono{4cN>Vr&>L4kXH;_VnBL5U!LgzqE%P7QQ*<E!guRW2SE@ayq@)G2nXqA2tGo",
|
||||
"QIgc6>tue}O`3(TZ0`aKn&~8trOQ-rBXCp)f@P6RMO4l0+;b|5-pk9_ryNh}Zc*v%mvz_#",
|
||||
"yd<xXt%~gT90dn4e{Ac<baL-)Y{L7&5G($I$>6fjB0g9{MmMnu8bG%#C~ugXK^S^k@?ab#",
|
||||
"O|aE>dDTt4s4n69(~@t~!wniV%g<uWQat_i6>7khFx~I*4>Y|V$4j5%KPF*-FyKIi@!Ho&",
|
||||
"x8QQsksYt8)D+W)Ni!=G`ogSu^vLL-l#7A7=iIAKL2SuZk9F}NfNk86VI)9WZE?%2wC-ya",
|
||||
"F~z#Qsq)LH0|_D8^5fU8X%GeQ4TB>R-dlziA&tZe&1ada208!$nk`7bOFO2S00G<w{Sp8G",
|
||||
"{cR_IvBYQl0ssI200dcD"
|
||||
],
|
||||
"America/Los_Angeles": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0qH3OkDsf7KxBg5R*;z{h&-RlhRYu$%jt%!jv+I",
|
||||
"JxhE=%W1?wYb!37Rb?(rgwFIAQI{L#8r*zy!$TMtER_1(vn(Zix^{AVB1(jwr$iL6h0Z!2",
|
||||
"8Gb~UW@0~e512{Z%8}Qzdnjl~wJ1{c2>`Z@1A~t&lyL{p{eM{5)QGf7Mo5FW9==mlyXJt2",
|
||||
"UwpntR7H0eSq!(aYq#aqUz&RM*tvuMI)AsM?K3-dV3-TT{t)!Iy#JTo=tXkzAM9~j2YbiO",
|
||||
"ls3(H8Dc>Y|D1aqL51vjLbpYG;GvGTQB4bXuJ%mA;(B4eUpu$$@zv2vVcq-Y)VKbzp^tei",
|
||||
"uzy}R{Luv<C;_cPe*n$Z<jeC9ogWF9=1mvvUYXS>DjpuVb`79O+CBmg{Wx!bvx$eu4zRE&",
|
||||
"PehMb=&G<9$>iZ|bFE)0=4I?KLFGBC0I(0_svgw0%FiMsT%koo*!nEYc6GY@QnU}&4Isg;",
|
||||
"l=|khi(!VaiSE2=Ny`&&tpi~~;{$u<GHlsr3Ze!iYsU205RFKsLnrXwOL?Mq08xffgS{6h",
|
||||
"E|figx+&N%wbO}re@|}$l;g_6J-Wl%j|qev8A<T?NJ)`;2neGi_DHE4ET*W!c*ggPAgU+L",
|
||||
"E9=bH7;maCUikw^R)UM;TdVvNkQ;FGgN=yQER`SZ1nOgPXr0LCebLety&}kVdmVmB=8eSg",
|
||||
"td!1%p=a2wooIL!Da}OPXvKBfRo?YxqS>N}%f|7mBhAy;<Er2&_LfND#qXN~Mkgf!@4VFA",
|
||||
"Hr%$c)wrKA2cJYWK2>s3YT^sy!$eG~?`9mNJC9@4Bac_p^BZh)Yd_rWW5qh-?tKY(>5VHO",
|
||||
"L*iT8P@wCavLj^yYbnDR+4ukhS+xPrpl)iqB?u)bj9a2aW==g6G3lCJd>(+Blf<d4CF%7u",
|
||||
"tlBUDki}J-!_Dy}5S(MrxSXy~$Z+hgH3P^<<w7D72L7I-R%H3(xm&q_DXxkp$owLTS6Wzk",
|
||||
"hc3nn;laROa3)6hl&gH#)2Lif8fZe$@CdeJ-Zn&*>r)~^40F4f>cRZ^UF;RibfZ>0m73hR",
|
||||
"C{$vTfC(STN`g7(B<=Z2556{}0`?p&|Akkst!4Xy4OT;A@c$XTUI3FRRjy*KA7uC56FD)z",
|
||||
"^X{WV*sr(w!c$W357o!&eLO2wTDNOyw@gf(&R<<LF_3URI4=Ei`-%dM3T66j#9!aG7&b_@",
|
||||
"g1-9vo?DzXZ5vGaf~w__p_@_X?OdvQ_r5bvy2hpESTf+{p?jL+!~!{g8-<-5$@d8EZV&-5",
|
||||
"@a|;^1gB*R-~{EHFA-td_G2bt;~Y}>t;=-Tu1TV{>%8ZVATC9tjD8|(&`$9YHvZ9bVe#>w",
|
||||
"|8c;Tg|xE&)`*}LwM*E}q}q8^Qja%p`_U)*5DdLI9O@!e=3jFjOCrCq28b_bb;s>%D#iJB",
|
||||
"CWJi{JH!Js;6nfayos$kq^OEX00HO-lokL0!mqm{vBYQl0ssI200dcD"
|
||||
],
|
||||
"America/Santiago": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0fRZ<6QtM7KxBg84(fsEAUJ$J{f-TXlPEUec5Ee",
|
||||
"n+hsD4lC(QYax=JdSpoyje8%VM`GW}<Unz6IOY4=y66tfqG2X4E8xIJQ(~?r{`L~T!sI~o",
|
||||
"VBl7Ao!R1A76Y8P6Y<TfwVHf@sl@S-D4OuAy5mq0MKJZ>{bJ8@y$A8O&*$pw{(f~Os#}2w",
|
||||
"eX6^Rgi$IT%n^V^85L>$_c7{cB^#ogV=rHBJGiz-RQNFGK?gdPi|q)j`&8)}KJ{qo6dixa",
|
||||
"9@yYyVg+%lo0nO+Tw0-w2hJ%mafy<Co(;L+24CYl&?rN0mrh90nxG?%1&Ed@za`Yd>WL)|",
|
||||
")<o0dZL-*?RFtH7dAv%G*O%l?qvq!0F5C?K#_ZoT{P$77IMoj3&8w3f&n36zquu~s`s0T)",
|
||||
";>?W6Bi%FWuGPA1Dru$XR4SZANsAthU2EoKH<MU4wYvUTlZGcLIDR+hSik>F6oEtKq`rwP",
|
||||
"(VNegnI_NI%;ma$)wj{k!@KFB30Yo)IOr<QX7IQ@TBq9d;e3QAtYU?$PS-WoaiqwFrg4PR",
|
||||
"A->l>)$)D|+(5h&+%2vuwGuy^@S8FT^s21V5};>VA9Iu;?8bHz#r<;JtfZDI1(FT@edh0#",
|
||||
"MYW$A1qkMGIwTZqqdYNE3gl#zp&NbL9Mp=voqN|;?gqR&4$)1`znddtEyuKS*^nMMD=0^>",
|
||||
"7^z6-C4P67UWOXuMBubP>j6i~03aR@jD^-Y`JSYu#Yp0P8dLLJ0QOPE8=BoiuRX59YW7xg",
|
||||
"WiexjHX%&0?`ZQCdxCdL^qd1v@kOjQKaWo2Y1++~LcA%FTq?5o<?(jL(_Uo}I}k_Fwflcr",
|
||||
"aovwSR_(ILA6li<iBLPQ0#rEet;W-*54kj#sZEGK*tAF{)HNkn#&Hc5`#eaRF;N#$<xQU?",
|
||||
"E%zm?2+b5Ho>%}fX1-RIvlB)1#iTNomGnUL=nM!>Ix|AGtON7!F1O?53kqlC2o-`ZGw*+s",
|
||||
"NM$^9znsIJMwlgscE`|O3|;BRgsQMYm~`uv+nvuv`nigRa}X=BX=A5Sw$)WEklF7&c>_~$",
|
||||
"zJ(m--bqXgiN^w-U=BJH9C0Qro(x90zo@rK;&TJ$nI@&k$ORgOb2<MjjIhYfr;pFUGdMd!",
|
||||
"0d&bOvyq3AZPCez8E(XSg2hBu2A&^k?w|1u8v3JE>s%gWbc}ok_27)Eoku~Fq|B-Ps+4J_",
|
||||
"HPJMLJ2^_)cOU$p&3kNAlrV!)%~6r$BJ>OOi~=-<6byle{?zd4J{NG}o8tw|+#ZNLcpNwk",
|
||||
"TuPE~sbJB8_RZb2DopStO+Wwux~F#S59zm%00I98;S&G=b(j+6vBYQl0ssI200dcD"
|
||||
],
|
||||
"Asia/Tokyo": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~luMgIxeB7KxBg5R*;y?l4Rl4neXH3cv!OtfK@h",
|
||||
"KZzauI)S!FSDREPhhBS6Fb$&Vv#7%;?Te|>pF^0HBr&z_Tk<%vMW_QqjevRZOp8XVFgP<8",
|
||||
"TkT#`9H&0Ua;gT1#rZLV0HqbAKK;_z@nO;6t0L<i8TZ+%T<;ci2bYSG1u!mUSO5S3XcbN8",
|
||||
"dIxbZ00Ex?wE_SDJu@vkvBYQl0ssI200dcD"
|
||||
],
|
||||
"Australia/Sydney": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0T)o7+nA=7KxBg5R*_t6jS5T`_Ull(nK1_YY;k%",
|
||||
";_YdTuU3*!K)eKg@^kzjAtbo@Jd|KGai=Q%%sX5FI?*?LG!|m9cKH5~IEwI=PAr_Yc}w35",
|
||||
">}hOdk<>TdUa07R(LPI6@!GU$ty4=mwqHG-XVe*n(Yvgdlr+FqIU18!osi)48t~eWX8)&L",
|
||||
"G)Ud^0zz@*AF+2r7E}N<P$kOfo*88g)_bOO?7N1Jr|HJyg+HXc7f4}?%Dur3w|~JU?<x4K",
|
||||
"%RRC~q_D87;UyN{nLRu!fEqKeRR*U$vs>f9Y72K~o-T%}D&z%}#7g<qim`EbfhF7ntyAiP",
|
||||
"%LFNc&!$@Kv)Olyf&Y9%(#SkM+%yI}S%b+@ZM2dH7DpmndGMIda<(`#E9q|?H(HzClx+l;",
|
||||
"M?IEz1eF}r?}ay!V9?9rKD^-ayjE@wUMD$2kC!iwH`n=eVrJPmJyNKaW`LdJ68&u;2nF1K",
|
||||
"kZjKCY_A<>2br?oH6ZiYH^%>J3D)TPKV(JY*bwjuw5=DsPB@~CrR<E_U_fJTF9ufU%!cXK",
|
||||
"_4uM#!%%Q1e1G~{E}~vGVE0{Kxecm^NjtJM`c8EFHFTiUIVl@YUD8F+s!u8jz~6hte@oa|",
|
||||
"qayb*^Lwd(etNmBro;aXQjkY8g(*`_JQ0%{V3QP2l!GGQ7D+v&k_PK0F(?f{GziU5>OZeN",
|
||||
"x>A*H&CHrWt0`EP`m!F%waepl#|w#&`XgVc?~2M3uw$fGX~tf_Il!q#Aa<*8xlzQ2+7r6Z",
|
||||
"^;Laa9F(WB_O&Dy2r>~@kSi16W{=6+i5GV=Uq~KX*~&HUN4oz7*O(gXIr}sDVcD`Ikgw#|",
|
||||
"50ssal8s)Qy;?YGCf;*UKKKN!T4!Kqy_G;7<gSrPK{)5#a>PfQapugqvVBKy12v3TVH^L2",
|
||||
"0?#5*VP~MOYfe$h`*L!7@tiW|_^X1N%<}`7YahiUYtMu5XwmOf3?dr+@zXHwW`z}ZDqZlT",
|
||||
"<2Cs(<1%M!i6o&VK89BY0J7HPIo;O62s=|IbV^@y$N&#<x=a876<(U>=>i^F00FcHoDl#3",
|
||||
"Mdv&xvBYQl0ssI200dcD"
|
||||
],
|
||||
"Europe/Dublin": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0>b$_+0=h7KxBg5R*;&J77#T_U2R5sleVWFDmK~",
|
||||
"Kzj5oh@`<njquRZ&tJIS(cXp1>QKHvW^6V{jU-w>qg1tSt0c^vh;?qAqA0%t?;#S~6U8Qi",
|
||||
"v&f1s9IH#g$m1k1a#3+lylw4mwT4QnEUUQdwg+xnEcBlgu31bAVabn41OMZVLGz6NDwG%X",
|
||||
"uQar!b>GI{qSahE`AG}$kRWbuI~JCt;38)Xwbb~Qggs55t+MAHIxgDxzTJ;2xXx99+qCy4",
|
||||
"45kC#v_l8fx|G&jlVvaciR<-wwf22l%4(t@S6tnX39#_K(4S0fu$FUs$isu<UOJYm|4)2i",
|
||||
"aEpsajn@}B#rnY=Cg_TXsm-A)*adXV&$klNTn3n{XXlaquu}6m{k%oRmY0Yyhlj*<W{D5m",
|
||||
"22}OiqnwHT!tnK`wPqx?wiF%v{ipTrOkcJ5P@7OC4(-l`*&SB$Wd4Vf8gn?>d<i@%mP*e*",
|
||||
"ttDj`9M1;9$YV@dhT)DVcwdq(Ly~KDm_&KL?{_mFwwYtJqRZBk)i1FVQy!40w_KyAg?hIA",
|
||||
"=_{(3#S0eWsF8f%_4Zza$4@$lSmov+Huyn$vP^zJ|8-<C3#q#0kEs9cNg^xUR(m?wEWt-D",
|
||||
"GctAh2nIo~fz%$m$I41=b_WuJ6M9g#A9_Epwqw{d0B|vzmg#_y<=_>9IKzCXB<o`d)**5V",
|
||||
"6g!<<Jw1n5TrN-$)aYz4cLsTmpsUf-6L7ix+kk>78NkARYq@9Dc0TGkhz);NtM_SSzEffN",
|
||||
"l{2^*CKGdp52h!52A)6q9fUSltXF{T*Ehc9Q7u8!W7pE(Fv$D$cKUAt6wY=DA1mGgxC*VX",
|
||||
"q_If3G#FY6-Voj`fIKk`0}Cc72_SD{v>468LV{pyBI33^p0E?}RwDA6Pkq--C~0jF&Z@Pv",
|
||||
"!dx_1SN_)jwz@P$(oK%P!Tk9?fRjK88yxhxlcFtTjjZ$DYssSsa#ufYrR+}}nKS+r384o~",
|
||||
"!Uw$nwTbF~qgRsgr0N#d@KIinx%<pnyQ!|>hQB(SJyjJtDtIy(%mDm}ZBGN}dV6K~om|=U",
|
||||
"VGkbciQ=^$_14|gT21!YQ)@y*Rd0i_lS6gtPBE9+ah%WIJPwzUTjIr+J1XckkmA!6WE16%",
|
||||
"CVAl{Dn&-)=G$Bjh?bh0$Xt1UDcgXJjXzzojuw0>paV~?Sa`VN3FysqF<S*L0RYSAY3jt(",
|
||||
"8wCD04RfyEcP(RNT%x7k(7m-9H3{zuQ`RZy-Rz%*&dldDVFF+TwSAPO1wRX^5W5@xJ9{vW",
|
||||
"w?rc^NH({%Ie<rxKqSVy!Le-_`U&@W_(D+>xTzfKVAu*ucq#+m=|KSSMvp_#@-lwd+q*ue",
|
||||
"FQ^5<D+|jLr?k{O39i8AX2Qb^zi9A<7XD1y!-W2|0Hk8JVkN;gl><|<0R-u4qYMbRqzSn&",
|
||||
"Q7jSuvc%b+EZc%>nI(+&0Tl1Y>a6v4`uNFD-7$QrhHgS7Wnv~rDgfH;rQw3+m`LJxoM4v#",
|
||||
"gK@?|B{RHJ*VxZgk#!p<_&-sjxOda0YaiJ1UnG41VPv(Et%ElzKRMcO$AfgU+Xnwg5p2_+",
|
||||
"NrnZ1WfEj^fmHd^sx@%JWKkh#zaK0ox%rdP)zUmGZZnqmZ_9L=%6R8ibJH0bOT$AGhDo6{",
|
||||
"fJ?;_U;D|^>5by2ul@i4Zf()InfFN}00EQ=q#FPL>RM>svBYQl0ssI200dcD"
|
||||
],
|
||||
"Europe/Lisbon": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0=rf*IfWA7KxBg5R*;*X|PN+G3LqthM?xgkNUN_",
|
||||
")gCt1Sc%YT6^TTomk4yVHXeyvQj8}l<;q&s7K}#Vnc8lII1?)AHh$*>OKUU4S;*h>v*ep0",
|
||||
"xTi1cK2{aY*|2D*-~K<;-{_W+r@NvZ7-|NZv($ek_C%VfP0xjWeZP#CPXD`IKkakjh(kUd",
|
||||
"&H)m;^Q(jGjIyiyrcUMtOP)u3A>sw6ux;Bmp3x$4QvQKMx5TrCx_!$srWQuXNs&`9=^IY1",
|
||||
"yc&C31!sQh7P=Mk*#6x8Z@5^%ehR8UW<EvzdWer9z;R6PrdUaWab3G>$OWw0KMw}P1ycI^",
|
||||
"4eh12oBUOV?S>n*d!+EM@>x#9PZD12iD=zaC;7`8dTfkU_6d}OZvSFSbGgXeKw}XyX@D=(",
|
||||
")D0!^DBGr8pXWBT$S-yhLP>Z3ys^VW<kSQr?{jhl<+{Fki;mTI=&Stgy$rttN?ulQM$lDr",
|
||||
"G7))C7Dx=J6V-e^(Qk|r;f~TvIw1KqRIC{8f^jPy#blstV{-&2a}ZJe!Zr2c_R4NT)L@bs",
|
||||
"+gRRm6Wn)VWVNHeK*TEV=f#2KZqu%y?mTx#EfRiK0)TG7$$~=LGxx@0D|lS2up|oCON{YQ",
|
||||
"oN5-H$!_n-Kx2*=RO!epEX>3}RQ6{NGGVJG6vf*MH93vvNW6yLjie1;{4tVhg-KnSf|G`!",
|
||||
"Z;j$7gJ1ows~RD=@n7I6aFd8rOR_7Y?E-$clI%1o5gA@O!KPa^(8^iFFeFykI-+z>E$mvp",
|
||||
"E_h`vbHPjqkLs`Dn-0FV`R@z|h!S(Lb;M&|Exr<u8#s-T(>!biY`%bfp$6`hK;GDhdP|^Q",
|
||||
"*Ty*}1d41K>H2B{jrjE9aFK>yAQJBX9CD%-384S;0fw`PlprHGS`^b$oS-`I4VH7ji8ou-",
|
||||
"g|060jfb1XcxiInT0oO<S+<vh^)XY;lr@|IeXj}%k;}|kSlDGaYidk^zB|gEYaet~F%QYd",
|
||||
"f7pbnQKLZ0o7=kso86doS;J@aQ>oeR7#%e5Ug5#KW)nV<Rc;|LjUDdhk8*dYJQwYN?hzH%",
|
||||
"0<XB$!(rpf2nxaL22M`L4pKx>SRvLHNe$SQHM@2)`S9L7>RL@<XAlxVQfb2=%lcu!h+Um0",
|
||||
"Q+Z=itevTFy}-Jl<g5crK55BF`VsoPH~qP3QrG%YtrD#s{=gA7p)QI<i=EwY(cel8`B=#u",
|
||||
"Yq<K;4T(QBF_GvrYueSk*}gfrCSg22+YH-1N<WYkp|DA-P-&va<Xu<}^yafJKlzezB-lS{",
|
||||
"a++P_^gYmgrc9FO-K3s~`jAcqVV!k?NV2IFV^86`cr>Qx%fmm7?3u7P5TywFQ}C@S(pq}|",
|
||||
"eLPT{C^{<0Q?uU&kSVd%!~8q3;Z0s3OqzF`$HRkePL5Ywgiwn{R(<RY8ut&RJ;$?J*w*n)",
|
||||
">zi+jmOBFrVpW;)@UsU#%$8BcV#h@}m$#!Fglo&bwb78aYqOG_W7h{eb(+39&-mk4EIXq_",
|
||||
"_`30=8sfA3=!3TO_TyS5X22~?6nKngZ|bq=grdq=9X)3xAkA42L!~rmS)n3w-~;lgz%Fhn",
|
||||
"(?rXdp2ho~9?wmVs2JwVt~?@FVD%`tN69{(i3oQa;O0<Hp#T5?$WIy3h`IlL00Hv}jT-;}",
|
||||
"Z2tpNvBYQl0ssI200dcD"
|
||||
],
|
||||
"Europe/London": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j;0`|pJ6!-O7KxBg5R*;$9DqzW!kQs3DZt(=0_!m1",
|
||||
"4wvE`6N%Vj#u6PS_3S?~(2)&xn8}2}3Wr#kG8n2!x8>$E$lF&~Y#_H6bu6(BiwblJ>;-Fs",
|
||||
"gA$Y$*?=X)n1pFkKn}F~`>=4)+LLQk?L*P!bhAm0;`N~z3QbUIyVrm%kOZ(n1JJsm0pyb8",
|
||||
"!GV{d*C!9KXv;4v<seWRpo=ZZxGf)-5Qsn$3dw`uhF)+6#mgUoNF-Y2jN73pVhdTs*p0`Z",
|
||||
"AbnT1puEtudB{Nul>D4Q>-k#+x(!V5L@w5M>v2V5<gcLskF+p`aGTSn{sY8^@MUc;2o{&V",
|
||||
"R!$180N}BtfYKS)i9w=!<~&l?1Cv^PWs!&a9{s(35^yqGU$72DKX|IkRtDblB>a`B>t(|B",
|
||||
"|Fqr4^-{S*%Ep~ojUtx_CRbSQ(uFwu2=KH)Q@EBs@ZqRXn4mU;B!68;;IQs3Ub=n&UU%*m",
|
||||
"k&zwD36&JSwsN(%k&x?H+tN^6)23c`I0=5^N_R0~1>tsFZ`^`3z~rXSXT&qcwa#n!%+Z#P",
|
||||
"PG}(D^_CCILXnF|GKwabBh*xFS?4rwGo2vtJUwzrbv_$5PO+`?$l{H-jGB@X%S!OAhw;D4",
|
||||
"XFycN3!XqQ&EorJOD3>~^U%Luw!jF<;6_q-f-S|6<EHry?%{@fuyH`_+D%uTA@g0$5e!Yi",
|
||||
"P1vQuevyS;jE(-R>{cQDfZ2(4Xf1MMLr1=SA=MwVf2%Pp%VP;jn)|5Tf!-DbUGn%I-r<KG",
|
||||
"4jJ(Y#L-fJUpUb$yNfvhX*iqWZoG7T*WUfE6iQD9_^EWqExH`rc&jJ<o^E8-mM10WrZ_Vv",
|
||||
"xx9nj<vMlEt*KfP*pyth!c_AKnrKtQTACX08#{pioAFnDq!53+h*hO^f*yrWjg0u2pUcgv",
|
||||
"UlpEZ9G_dlhlW1J^h@gTt7{KPL2mRal;1juJ3Q8-!GXO#IPzT4ciJ-nB+nkphssM}Q7IAT",
|
||||
"pM}AT%y(J!78F?>kYaH7?$$O!t)wwClAisr3eUoeB^~T=U*_P~Y2*KdnO87>B!19sV=xZ5",
|
||||
"yApq26RxgqA|*tmsvtL#OhcF(C<0EGWHP)BF<g*iSWicU6k1<Ps?BQ$IWg-#s2uF-qXgJ_",
|
||||
"!H_mZIMx*L%&a*_6;_trMCULk0ZYM<hfJlYBddHwRyYUDu3!C_lJZWTQ?c-R&@9054pj0k",
|
||||
"kQ{Xi{A$&)&b#^G*}8w^qE5i<@aDxaJQs2E$W)AIqUXO{gQ;U8|FA%BD~sORzq44)AntUu",
|
||||
"QHBO{{Pi<EpK!$x4(~7w)la!dN=M@L_j};6|5G&QfuO~2?Q7996z)78fqW<D#8tKNV(*qc",
|
||||
"mfA>l?h)_*7!{LoJiv%RsOs!q->n+DcV%9~B@Rb<ISu%16c5H-7zQIq+SuS+s<lQOWK5+C",
|
||||
"d*>C_1G_1g6`Yd~8|%-=2l~oGN!~TVv2Bnk>7wW8L@^?vX$f3AiT)(4nrCuTm9%(XC6Nai",
|
||||
"E(;}7&=YZagjAN$O-cN;1u{dTkElmB0GT$|Wa)QMmKrx<|LCJ9qlUoFsUbD^H^6_8(w<0{",
|
||||
"ftj&O1~p_%lh5z;zNV&sP<T$*OgK)_0B#JDtXOkhC;Bo7h)#RUy;vBiVLN-T$*7t*t9@ey",
|
||||
"3Woa&24QZ_z38BQ@A(A<(9n@%R?}B`7%w2wowt~UU;bAlqCzr(H$M5t==jGIqMqCsE=Jwa",
|
||||
"$3P+3^&|~i28@=d_u6Cgthe(Lq(wxKpdSDL|7X6Un<nrt00Gwuz#ISo`BbmvvBYQl0ssI2",
|
||||
"00dcD"
|
||||
],
|
||||
"Pacific/Kiritimati": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~jCaVO;<!7KxBg5R*{K!`A|q%C5j6({{dSEy5>+",
|
||||
"NF2>iK{8KMUf+)<-)VxXbLxD(alL}N$AT-ogNbJSMMYeX+Z{jS)b8TK^PB=FxyBxzfmFto",
|
||||
"eo0R`a(%NO?#aEH9|?Cv00000NIsFh6BW2800DjO0RR918Pu^`vBYQl0ssI200dcD"
|
||||
],
|
||||
"UTC": [
|
||||
"{Wp48S^xk9=GL@E0stWa761SMbT8$j-~e#|9bEt_7KxBg5R*|3h1|xhHLji!C57qW6L*|H",
|
||||
"pEErm00000ygu;I+>V)?00B92fhY-(AGY&-0RR9100dcD"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"version": "2020a"
|
||||
}
|
||||
}
|
||||
2259
Lib/test/test_zoneinfo/test_zoneinfo.py
vendored
Normal file
2259
Lib/test/test_zoneinfo/test_zoneinfo.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
368
Lib/test/test_zoneinfo/test_zoneinfo_property.py
vendored
Normal file
368
Lib/test/test_zoneinfo/test_zoneinfo_property.py
vendored
Normal file
@@ -0,0 +1,368 @@
|
||||
import contextlib
|
||||
import datetime
|
||||
import os
|
||||
import pickle
|
||||
import unittest
|
||||
import zoneinfo
|
||||
|
||||
from test.support.hypothesis_helper import hypothesis
|
||||
|
||||
import test.test_zoneinfo._support as test_support
|
||||
|
||||
ZoneInfoTestBase = test_support.ZoneInfoTestBase
|
||||
|
||||
py_zoneinfo, c_zoneinfo = test_support.get_modules()
|
||||
|
||||
UTC = datetime.timezone.utc
|
||||
MIN_UTC = datetime.datetime.min.replace(tzinfo=UTC)
|
||||
MAX_UTC = datetime.datetime.max.replace(tzinfo=UTC)
|
||||
ZERO = datetime.timedelta(0)
|
||||
|
||||
|
||||
def _valid_keys():
|
||||
"""Get available time zones, including posix/ and right/ directories."""
|
||||
from importlib import resources
|
||||
|
||||
available_zones = sorted(zoneinfo.available_timezones())
|
||||
TZPATH = zoneinfo.TZPATH
|
||||
|
||||
def valid_key(key):
|
||||
for root in TZPATH:
|
||||
key_file = os.path.join(root, key)
|
||||
if os.path.exists(key_file):
|
||||
return True
|
||||
|
||||
components = key.split("/")
|
||||
package_name = ".".join(["tzdata.zoneinfo"] + components[:-1])
|
||||
resource_name = components[-1]
|
||||
|
||||
try:
|
||||
return resources.files(package_name).joinpath(resource_name).is_file()
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
|
||||
# This relies on the fact that dictionaries maintain insertion order — for
|
||||
# shrinking purposes, it is preferable to start with the standard version,
|
||||
# then move to the posix/ version, then to the right/ version.
|
||||
out_zones = {"": available_zones}
|
||||
for prefix in ["posix", "right"]:
|
||||
prefix_out = []
|
||||
for key in available_zones:
|
||||
prefix_key = f"{prefix}/{key}"
|
||||
if valid_key(prefix_key):
|
||||
prefix_out.append(prefix_key)
|
||||
|
||||
out_zones[prefix] = prefix_out
|
||||
|
||||
output = []
|
||||
for keys in out_zones.values():
|
||||
output.extend(keys)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
VALID_KEYS = _valid_keys()
|
||||
if not VALID_KEYS:
|
||||
raise unittest.SkipTest("No time zone data available")
|
||||
|
||||
|
||||
def valid_keys():
|
||||
return hypothesis.strategies.sampled_from(VALID_KEYS)
|
||||
|
||||
|
||||
KEY_EXAMPLES = [
|
||||
"Africa/Abidjan",
|
||||
"Africa/Casablanca",
|
||||
"America/Los_Angeles",
|
||||
"America/Santiago",
|
||||
"Asia/Tokyo",
|
||||
"Australia/Sydney",
|
||||
"Europe/Dublin",
|
||||
"Europe/Lisbon",
|
||||
"Europe/London",
|
||||
"Pacific/Kiritimati",
|
||||
"UTC",
|
||||
]
|
||||
|
||||
|
||||
def add_key_examples(f):
|
||||
for key in KEY_EXAMPLES:
|
||||
f = hypothesis.example(key)(f)
|
||||
return f
|
||||
|
||||
|
||||
class ZoneInfoTest(ZoneInfoTestBase):
|
||||
module = py_zoneinfo
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_str(self, key):
|
||||
zi = self.klass(key)
|
||||
self.assertEqual(str(zi), key)
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_key(self, key):
|
||||
zi = self.klass(key)
|
||||
|
||||
self.assertEqual(zi.key, key)
|
||||
|
||||
@hypothesis.given(
|
||||
dt=hypothesis.strategies.one_of(
|
||||
hypothesis.strategies.datetimes(), hypothesis.strategies.times()
|
||||
)
|
||||
)
|
||||
@hypothesis.example(dt=datetime.datetime.min)
|
||||
@hypothesis.example(dt=datetime.datetime.max)
|
||||
@hypothesis.example(dt=datetime.datetime(1970, 1, 1))
|
||||
@hypothesis.example(dt=datetime.datetime(2039, 1, 1))
|
||||
@hypothesis.example(dt=datetime.time(0))
|
||||
@hypothesis.example(dt=datetime.time(12, 0))
|
||||
@hypothesis.example(dt=datetime.time(23, 59, 59, 999999))
|
||||
def test_utc(self, dt):
|
||||
zi = self.klass("UTC")
|
||||
dt_zi = dt.replace(tzinfo=zi)
|
||||
|
||||
self.assertEqual(dt_zi.utcoffset(), ZERO)
|
||||
self.assertEqual(dt_zi.dst(), ZERO)
|
||||
self.assertEqual(dt_zi.tzname(), "UTC")
|
||||
|
||||
|
||||
class CZoneInfoTest(ZoneInfoTest):
|
||||
module = c_zoneinfo
|
||||
|
||||
|
||||
class ZoneInfoPickleTest(ZoneInfoTestBase):
|
||||
module = py_zoneinfo
|
||||
|
||||
def setUp(self):
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(test_support.set_zoneinfo_module(self.module))
|
||||
self.addCleanup(stack.pop_all().close)
|
||||
|
||||
super().setUp()
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_pickle_unpickle_cache(self, key):
|
||||
zi = self.klass(key)
|
||||
pkl_str = pickle.dumps(zi)
|
||||
zi_rt = pickle.loads(pkl_str)
|
||||
|
||||
self.assertIs(zi, zi_rt)
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_pickle_unpickle_no_cache(self, key):
|
||||
zi = self.klass.no_cache(key)
|
||||
pkl_str = pickle.dumps(zi)
|
||||
zi_rt = pickle.loads(pkl_str)
|
||||
|
||||
self.assertIsNot(zi, zi_rt)
|
||||
self.assertEqual(str(zi), str(zi_rt))
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_pickle_unpickle_cache_multiple_rounds(self, key):
|
||||
"""Test that pickle/unpickle is idempotent."""
|
||||
zi_0 = self.klass(key)
|
||||
pkl_str_0 = pickle.dumps(zi_0)
|
||||
zi_1 = pickle.loads(pkl_str_0)
|
||||
pkl_str_1 = pickle.dumps(zi_1)
|
||||
zi_2 = pickle.loads(pkl_str_1)
|
||||
pkl_str_2 = pickle.dumps(zi_2)
|
||||
|
||||
self.assertEqual(pkl_str_0, pkl_str_1)
|
||||
self.assertEqual(pkl_str_1, pkl_str_2)
|
||||
|
||||
self.assertIs(zi_0, zi_1)
|
||||
self.assertIs(zi_0, zi_2)
|
||||
self.assertIs(zi_1, zi_2)
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_pickle_unpickle_no_cache_multiple_rounds(self, key):
|
||||
"""Test that pickle/unpickle is idempotent."""
|
||||
zi_cache = self.klass(key)
|
||||
|
||||
zi_0 = self.klass.no_cache(key)
|
||||
pkl_str_0 = pickle.dumps(zi_0)
|
||||
zi_1 = pickle.loads(pkl_str_0)
|
||||
pkl_str_1 = pickle.dumps(zi_1)
|
||||
zi_2 = pickle.loads(pkl_str_1)
|
||||
pkl_str_2 = pickle.dumps(zi_2)
|
||||
|
||||
self.assertEqual(pkl_str_0, pkl_str_1)
|
||||
self.assertEqual(pkl_str_1, pkl_str_2)
|
||||
|
||||
self.assertIsNot(zi_0, zi_1)
|
||||
self.assertIsNot(zi_0, zi_2)
|
||||
self.assertIsNot(zi_1, zi_2)
|
||||
|
||||
self.assertIsNot(zi_0, zi_cache)
|
||||
self.assertIsNot(zi_1, zi_cache)
|
||||
self.assertIsNot(zi_2, zi_cache)
|
||||
|
||||
|
||||
class CZoneInfoPickleTest(ZoneInfoPickleTest):
|
||||
module = c_zoneinfo
|
||||
|
||||
|
||||
class ZoneInfoCacheTest(ZoneInfoTestBase):
|
||||
module = py_zoneinfo
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_cache(self, key):
|
||||
zi_0 = self.klass(key)
|
||||
zi_1 = self.klass(key)
|
||||
|
||||
self.assertIs(zi_0, zi_1)
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_no_cache(self, key):
|
||||
zi_0 = self.klass.no_cache(key)
|
||||
zi_1 = self.klass.no_cache(key)
|
||||
|
||||
self.assertIsNot(zi_0, zi_1)
|
||||
|
||||
|
||||
class CZoneInfoCacheTest(ZoneInfoCacheTest):
|
||||
klass = c_zoneinfo.ZoneInfo
|
||||
|
||||
|
||||
class PythonCConsistencyTest(unittest.TestCase):
|
||||
"""Tests that the C and Python versions do the same thing."""
|
||||
|
||||
def _is_ambiguous(self, dt):
|
||||
return dt.replace(fold=not dt.fold).utcoffset() == dt.utcoffset()
|
||||
|
||||
@hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
|
||||
@hypothesis.example(dt=datetime.datetime.min, key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime.max, key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime(2020, 1, 1), key="Europe/Paris")
|
||||
@hypothesis.example(dt=datetime.datetime(2020, 6, 1), key="Europe/Paris")
|
||||
def test_same_str(self, dt, key):
|
||||
py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
|
||||
c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
|
||||
|
||||
self.assertEqual(str(py_dt), str(c_dt))
|
||||
|
||||
@hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
|
||||
@hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime(2020, 2, 5), key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime(2020, 8, 12), key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime(2040, 1, 1), key="Africa/Casablanca")
|
||||
@hypothesis.example(dt=datetime.datetime(1970, 1, 1), key="Europe/Paris")
|
||||
@hypothesis.example(dt=datetime.datetime(2040, 1, 1), key="Europe/Paris")
|
||||
@hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo")
|
||||
@hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo")
|
||||
def test_same_offsets_and_names(self, dt, key):
|
||||
py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
|
||||
c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
|
||||
|
||||
self.assertEqual(py_dt.tzname(), c_dt.tzname())
|
||||
self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset())
|
||||
self.assertEqual(py_dt.dst(), c_dt.dst())
|
||||
|
||||
@hypothesis.given(
|
||||
dt=hypothesis.strategies.datetimes(timezones=hypothesis.strategies.just(UTC)),
|
||||
key=valid_keys(),
|
||||
)
|
||||
@hypothesis.example(dt=MIN_UTC, key="Asia/Tokyo")
|
||||
@hypothesis.example(dt=MAX_UTC, key="Asia/Tokyo")
|
||||
@hypothesis.example(dt=MIN_UTC, key="America/New_York")
|
||||
@hypothesis.example(dt=MAX_UTC, key="America/New_York")
|
||||
@hypothesis.example(
|
||||
dt=datetime.datetime(2006, 10, 29, 5, 15, tzinfo=UTC),
|
||||
key="America/New_York",
|
||||
)
|
||||
def test_same_from_utc(self, dt, key):
|
||||
py_zi = py_zoneinfo.ZoneInfo(key)
|
||||
c_zi = c_zoneinfo.ZoneInfo(key)
|
||||
|
||||
# Convert to UTC: This can overflow, but we just care about consistency
|
||||
py_overflow_exc = None
|
||||
c_overflow_exc = None
|
||||
try:
|
||||
py_dt = dt.astimezone(py_zi)
|
||||
except OverflowError as e:
|
||||
py_overflow_exc = e
|
||||
|
||||
try:
|
||||
c_dt = dt.astimezone(c_zi)
|
||||
except OverflowError as e:
|
||||
c_overflow_exc = e
|
||||
|
||||
if (py_overflow_exc is not None) != (c_overflow_exc is not None):
|
||||
raise py_overflow_exc or c_overflow_exc # pragma: nocover
|
||||
|
||||
if py_overflow_exc is not None:
|
||||
return # Consistently raises the same exception
|
||||
|
||||
# PEP 495 says that an inter-zone comparison between ambiguous
|
||||
# datetimes is always False.
|
||||
if py_dt != c_dt:
|
||||
self.assertEqual(
|
||||
self._is_ambiguous(py_dt),
|
||||
self._is_ambiguous(c_dt),
|
||||
(py_dt, c_dt),
|
||||
)
|
||||
|
||||
self.assertEqual(py_dt.tzname(), c_dt.tzname())
|
||||
self.assertEqual(py_dt.utcoffset(), c_dt.utcoffset())
|
||||
self.assertEqual(py_dt.dst(), c_dt.dst())
|
||||
|
||||
@hypothesis.given(dt=hypothesis.strategies.datetimes(), key=valid_keys())
|
||||
@hypothesis.example(dt=datetime.datetime.max, key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime.min, key="America/New_York")
|
||||
@hypothesis.example(dt=datetime.datetime.min, key="Asia/Tokyo")
|
||||
@hypothesis.example(dt=datetime.datetime.max, key="Asia/Tokyo")
|
||||
def test_same_to_utc(self, dt, key):
|
||||
py_dt = dt.replace(tzinfo=py_zoneinfo.ZoneInfo(key))
|
||||
c_dt = dt.replace(tzinfo=c_zoneinfo.ZoneInfo(key))
|
||||
|
||||
# Convert from UTC: Overflow OK if it happens in both implementations
|
||||
py_overflow_exc = None
|
||||
c_overflow_exc = None
|
||||
try:
|
||||
py_utc = py_dt.astimezone(UTC)
|
||||
except OverflowError as e:
|
||||
py_overflow_exc = e
|
||||
|
||||
try:
|
||||
c_utc = c_dt.astimezone(UTC)
|
||||
except OverflowError as e:
|
||||
c_overflow_exc = e
|
||||
|
||||
if (py_overflow_exc is not None) != (c_overflow_exc is not None):
|
||||
raise py_overflow_exc or c_overflow_exc # pragma: nocover
|
||||
|
||||
if py_overflow_exc is not None:
|
||||
return # Consistently raises the same exception
|
||||
|
||||
self.assertEqual(py_utc, c_utc)
|
||||
|
||||
@hypothesis.given(key=valid_keys())
|
||||
@add_key_examples
|
||||
def test_cross_module_pickle(self, key):
|
||||
py_zi = py_zoneinfo.ZoneInfo(key)
|
||||
c_zi = c_zoneinfo.ZoneInfo(key)
|
||||
|
||||
with test_support.set_zoneinfo_module(py_zoneinfo):
|
||||
py_pkl = pickle.dumps(py_zi)
|
||||
|
||||
with test_support.set_zoneinfo_module(c_zoneinfo):
|
||||
c_pkl = pickle.dumps(c_zi)
|
||||
|
||||
with test_support.set_zoneinfo_module(c_zoneinfo):
|
||||
# Python → C
|
||||
py_to_c_zi = pickle.loads(py_pkl)
|
||||
self.assertIs(py_to_c_zi, c_zi)
|
||||
|
||||
with test_support.set_zoneinfo_module(py_zoneinfo):
|
||||
# C → Python
|
||||
c_to_py_zi = pickle.loads(c_pkl)
|
||||
self.assertIs(c_to_py_zi, py_zi)
|
||||
31
Lib/zoneinfo/__init__.py
vendored
Normal file
31
Lib/zoneinfo/__init__.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
__all__ = [
|
||||
"ZoneInfo",
|
||||
"reset_tzpath",
|
||||
"available_timezones",
|
||||
"TZPATH",
|
||||
"ZoneInfoNotFoundError",
|
||||
"InvalidTZPathWarning",
|
||||
]
|
||||
|
||||
from . import _tzpath
|
||||
from ._common import ZoneInfoNotFoundError
|
||||
|
||||
try:
|
||||
from _zoneinfo import ZoneInfo
|
||||
except ImportError: # pragma: nocover
|
||||
from ._zoneinfo import ZoneInfo
|
||||
|
||||
reset_tzpath = _tzpath.reset_tzpath
|
||||
available_timezones = _tzpath.available_timezones
|
||||
InvalidTZPathWarning = _tzpath.InvalidTZPathWarning
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "TZPATH":
|
||||
return _tzpath.TZPATH
|
||||
else:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def __dir__():
|
||||
return sorted(list(globals()) + ["TZPATH"])
|
||||
164
Lib/zoneinfo/_common.py
vendored
Normal file
164
Lib/zoneinfo/_common.py
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
import struct
|
||||
|
||||
|
||||
def load_tzdata(key):
|
||||
from importlib import resources
|
||||
|
||||
components = key.split("/")
|
||||
package_name = ".".join(["tzdata.zoneinfo"] + components[:-1])
|
||||
resource_name = components[-1]
|
||||
|
||||
try:
|
||||
return resources.files(package_name).joinpath(resource_name).open("rb")
|
||||
except (ImportError, FileNotFoundError, UnicodeEncodeError):
|
||||
# There are three types of exception that can be raised that all amount
|
||||
# to "we cannot find this key":
|
||||
#
|
||||
# ImportError: If package_name doesn't exist (e.g. if tzdata is not
|
||||
# installed, or if there's an error in the folder name like
|
||||
# Amrica/New_York)
|
||||
# FileNotFoundError: If resource_name doesn't exist in the package
|
||||
# (e.g. Europe/Krasnoy)
|
||||
# UnicodeEncodeError: If package_name or resource_name are not UTF-8,
|
||||
# such as keys containing a surrogate character.
|
||||
raise ZoneInfoNotFoundError(f"No time zone found with key {key}")
|
||||
|
||||
|
||||
def load_data(fobj):
|
||||
header = _TZifHeader.from_file(fobj)
|
||||
|
||||
if header.version == 1:
|
||||
time_size = 4
|
||||
time_type = "l"
|
||||
else:
|
||||
# Version 2+ has 64-bit integer transition times
|
||||
time_size = 8
|
||||
time_type = "q"
|
||||
|
||||
# Version 2+ also starts with a Version 1 header and data, which
|
||||
# we need to skip now
|
||||
skip_bytes = (
|
||||
header.timecnt * 5 # Transition times and types
|
||||
+ header.typecnt * 6 # Local time type records
|
||||
+ header.charcnt # Time zone designations
|
||||
+ header.leapcnt * 8 # Leap second records
|
||||
+ header.isstdcnt # Standard/wall indicators
|
||||
+ header.isutcnt # UT/local indicators
|
||||
)
|
||||
|
||||
fobj.seek(skip_bytes, 1)
|
||||
|
||||
# Now we need to read the second header, which is not the same
|
||||
# as the first
|
||||
header = _TZifHeader.from_file(fobj)
|
||||
|
||||
typecnt = header.typecnt
|
||||
timecnt = header.timecnt
|
||||
charcnt = header.charcnt
|
||||
|
||||
# The data portion starts with timecnt transitions and indices
|
||||
if timecnt:
|
||||
trans_list_utc = struct.unpack(
|
||||
f">{timecnt}{time_type}", fobj.read(timecnt * time_size)
|
||||
)
|
||||
trans_idx = struct.unpack(f">{timecnt}B", fobj.read(timecnt))
|
||||
else:
|
||||
trans_list_utc = ()
|
||||
trans_idx = ()
|
||||
|
||||
# Read the ttinfo struct, (utoff, isdst, abbrind)
|
||||
if typecnt:
|
||||
utcoff, isdst, abbrind = zip(
|
||||
*(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt))
|
||||
)
|
||||
else:
|
||||
utcoff = ()
|
||||
isdst = ()
|
||||
abbrind = ()
|
||||
|
||||
# Now read the abbreviations. They are null-terminated strings, indexed
|
||||
# not by position in the array but by position in the unsplit
|
||||
# abbreviation string. I suppose this makes more sense in C, which uses
|
||||
# null to terminate the strings, but it's inconvenient here...
|
||||
abbr_vals = {}
|
||||
abbr_chars = fobj.read(charcnt)
|
||||
|
||||
def get_abbr(idx):
|
||||
# Gets a string starting at idx and running until the next \x00
|
||||
#
|
||||
# We cannot pre-populate abbr_vals by splitting on \x00 because there
|
||||
# are some zones that use subsets of longer abbreviations, like so:
|
||||
#
|
||||
# LMT\x00AHST\x00HDT\x00
|
||||
#
|
||||
# Where the idx to abbr mapping should be:
|
||||
#
|
||||
# {0: "LMT", 4: "AHST", 5: "HST", 9: "HDT"}
|
||||
if idx not in abbr_vals:
|
||||
span_end = abbr_chars.find(b"\x00", idx)
|
||||
abbr_vals[idx] = abbr_chars[idx:span_end].decode()
|
||||
|
||||
return abbr_vals[idx]
|
||||
|
||||
abbr = tuple(get_abbr(idx) for idx in abbrind)
|
||||
|
||||
# The remainder of the file consists of leap seconds (currently unused) and
|
||||
# the standard/wall and ut/local indicators, which are metadata we don't need.
|
||||
# In version 2 files, we need to skip the unnecessary data to get at the TZ string:
|
||||
if header.version >= 2:
|
||||
# Each leap second record has size (time_size + 4)
|
||||
skip_bytes = header.isutcnt + header.isstdcnt + header.leapcnt * 12
|
||||
fobj.seek(skip_bytes, 1)
|
||||
|
||||
c = fobj.read(1) # Should be \n
|
||||
assert c == b"\n", c
|
||||
|
||||
tz_bytes = b""
|
||||
while (c := fobj.read(1)) != b"\n":
|
||||
tz_bytes += c
|
||||
|
||||
tz_str = tz_bytes
|
||||
else:
|
||||
tz_str = None
|
||||
|
||||
return trans_idx, trans_list_utc, utcoff, isdst, abbr, tz_str
|
||||
|
||||
|
||||
class _TZifHeader:
|
||||
__slots__ = [
|
||||
"version",
|
||||
"isutcnt",
|
||||
"isstdcnt",
|
||||
"leapcnt",
|
||||
"timecnt",
|
||||
"typecnt",
|
||||
"charcnt",
|
||||
]
|
||||
|
||||
def __init__(self, *args):
|
||||
for attr, val in zip(self.__slots__, args, strict=True):
|
||||
setattr(self, attr, val)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, stream):
|
||||
# The header starts with a 4-byte "magic" value
|
||||
if stream.read(4) != b"TZif":
|
||||
raise ValueError("Invalid TZif file: magic not found")
|
||||
|
||||
_version = stream.read(1)
|
||||
if _version == b"\x00":
|
||||
version = 1
|
||||
else:
|
||||
version = int(_version)
|
||||
stream.read(15)
|
||||
|
||||
args = (version,)
|
||||
|
||||
# Slots are defined in the order that the bytes are arranged
|
||||
args = args + struct.unpack(">6l", stream.read(24))
|
||||
|
||||
return cls(*args)
|
||||
|
||||
|
||||
class ZoneInfoNotFoundError(KeyError):
|
||||
"""Exception raised when a ZoneInfo key is not found."""
|
||||
181
Lib/zoneinfo/_tzpath.py
vendored
Normal file
181
Lib/zoneinfo/_tzpath.py
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
import os
|
||||
import sysconfig
|
||||
|
||||
|
||||
def _reset_tzpath(to=None, stacklevel=4):
|
||||
global TZPATH
|
||||
|
||||
tzpaths = to
|
||||
if tzpaths is not None:
|
||||
if isinstance(tzpaths, (str, bytes)):
|
||||
raise TypeError(
|
||||
f"tzpaths must be a list or tuple, "
|
||||
+ f"not {type(tzpaths)}: {tzpaths!r}"
|
||||
)
|
||||
|
||||
if not all(map(os.path.isabs, tzpaths)):
|
||||
raise ValueError(_get_invalid_paths_message(tzpaths))
|
||||
base_tzpath = tzpaths
|
||||
else:
|
||||
env_var = os.environ.get("PYTHONTZPATH", None)
|
||||
if env_var is None:
|
||||
env_var = sysconfig.get_config_var("TZPATH")
|
||||
base_tzpath = _parse_python_tzpath(env_var, stacklevel)
|
||||
|
||||
TZPATH = tuple(base_tzpath)
|
||||
|
||||
|
||||
def reset_tzpath(to=None):
|
||||
"""Reset global TZPATH."""
|
||||
# We need `_reset_tzpath` helper function because it produces a warning,
|
||||
# it is used as both a module-level call and a public API.
|
||||
# This is how we equalize the stacklevel for both calls.
|
||||
_reset_tzpath(to)
|
||||
|
||||
|
||||
def _parse_python_tzpath(env_var, stacklevel):
|
||||
if not env_var:
|
||||
return ()
|
||||
|
||||
raw_tzpath = env_var.split(os.pathsep)
|
||||
new_tzpath = tuple(filter(os.path.isabs, raw_tzpath))
|
||||
|
||||
# If anything has been filtered out, we will warn about it
|
||||
if len(new_tzpath) != len(raw_tzpath):
|
||||
import warnings
|
||||
|
||||
msg = _get_invalid_paths_message(raw_tzpath)
|
||||
|
||||
warnings.warn(
|
||||
"Invalid paths specified in PYTHONTZPATH environment variable. "
|
||||
+ msg,
|
||||
InvalidTZPathWarning,
|
||||
stacklevel=stacklevel,
|
||||
)
|
||||
|
||||
return new_tzpath
|
||||
|
||||
|
||||
def _get_invalid_paths_message(tzpaths):
|
||||
invalid_paths = (path for path in tzpaths if not os.path.isabs(path))
|
||||
|
||||
prefix = "\n "
|
||||
indented_str = prefix + prefix.join(invalid_paths)
|
||||
|
||||
return (
|
||||
"Paths should be absolute but found the following relative paths:"
|
||||
+ indented_str
|
||||
)
|
||||
|
||||
|
||||
def find_tzfile(key):
|
||||
"""Retrieve the path to a TZif file from a key."""
|
||||
_validate_tzfile_path(key)
|
||||
for search_path in TZPATH:
|
||||
filepath = os.path.join(search_path, key)
|
||||
if os.path.isfile(filepath):
|
||||
return filepath
|
||||
|
||||
return None
|
||||
|
||||
|
||||
_TEST_PATH = os.path.normpath(os.path.join("_", "_"))[:-1]
|
||||
|
||||
|
||||
def _validate_tzfile_path(path, _base=_TEST_PATH):
|
||||
if os.path.isabs(path):
|
||||
raise ValueError(
|
||||
f"ZoneInfo keys may not be absolute paths, got: {path}"
|
||||
)
|
||||
|
||||
# We only care about the kinds of path normalizations that would change the
|
||||
# length of the key - e.g. a/../b -> a/b, or a/b/ -> a/b. On Windows,
|
||||
# normpath will also change from a/b to a\b, but that would still preserve
|
||||
# the length.
|
||||
new_path = os.path.normpath(path)
|
||||
if len(new_path) != len(path):
|
||||
raise ValueError(
|
||||
f"ZoneInfo keys must be normalized relative paths, got: {path}"
|
||||
)
|
||||
|
||||
resolved = os.path.normpath(os.path.join(_base, new_path))
|
||||
if not resolved.startswith(_base):
|
||||
raise ValueError(
|
||||
f"ZoneInfo keys must refer to subdirectories of TZPATH, got: {path}"
|
||||
)
|
||||
|
||||
|
||||
del _TEST_PATH
|
||||
|
||||
|
||||
def available_timezones():
|
||||
"""Returns a set containing all available time zones.
|
||||
|
||||
.. caution::
|
||||
|
||||
This may attempt to open a large number of files, since the best way to
|
||||
determine if a given file on the time zone search path is to open it
|
||||
and check for the "magic string" at the beginning.
|
||||
"""
|
||||
from importlib import resources
|
||||
|
||||
valid_zones = set()
|
||||
|
||||
# Start with loading from the tzdata package if it exists: this has a
|
||||
# pre-assembled list of zones that only requires opening one file.
|
||||
try:
|
||||
with resources.files("tzdata").joinpath("zones").open("r") as f:
|
||||
for zone in f:
|
||||
zone = zone.strip()
|
||||
if zone:
|
||||
valid_zones.add(zone)
|
||||
except (ImportError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
def valid_key(fpath):
|
||||
try:
|
||||
with open(fpath, "rb") as f:
|
||||
return f.read(4) == b"TZif"
|
||||
except Exception: # pragma: nocover
|
||||
return False
|
||||
|
||||
for tz_root in TZPATH:
|
||||
if not os.path.exists(tz_root):
|
||||
continue
|
||||
|
||||
for root, dirnames, files in os.walk(tz_root):
|
||||
if root == tz_root:
|
||||
# right/ and posix/ are special directories and shouldn't be
|
||||
# included in the output of available zones
|
||||
if "right" in dirnames:
|
||||
dirnames.remove("right")
|
||||
if "posix" in dirnames:
|
||||
dirnames.remove("posix")
|
||||
|
||||
for file in files:
|
||||
fpath = os.path.join(root, file)
|
||||
|
||||
key = os.path.relpath(fpath, start=tz_root)
|
||||
if os.sep != "/": # pragma: nocover
|
||||
key = key.replace(os.sep, "/")
|
||||
|
||||
if not key or key in valid_zones:
|
||||
continue
|
||||
|
||||
if valid_key(fpath):
|
||||
valid_zones.add(key)
|
||||
|
||||
if "posixrules" in valid_zones:
|
||||
# posixrules is a special symlink-only time zone where it exists, it
|
||||
# should not be included in the output
|
||||
valid_zones.remove("posixrules")
|
||||
|
||||
return valid_zones
|
||||
|
||||
|
||||
class InvalidTZPathWarning(RuntimeWarning):
|
||||
"""Warning raised if an invalid path is specified in PYTHONTZPATH."""
|
||||
|
||||
|
||||
TZPATH = ()
|
||||
_reset_tzpath(stacklevel=5)
|
||||
772
Lib/zoneinfo/_zoneinfo.py
vendored
Normal file
772
Lib/zoneinfo/_zoneinfo.py
vendored
Normal file
@@ -0,0 +1,772 @@
|
||||
import bisect
|
||||
import calendar
|
||||
import collections
|
||||
import functools
|
||||
import re
|
||||
import weakref
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
|
||||
from . import _common, _tzpath
|
||||
|
||||
EPOCH = datetime(1970, 1, 1)
|
||||
EPOCHORDINAL = datetime(1970, 1, 1).toordinal()
|
||||
|
||||
# It is relatively expensive to construct new timedelta objects, and in most
|
||||
# cases we're looking at the same deltas, like integer numbers of hours, etc.
|
||||
# To improve speed and memory use, we'll keep a dictionary with references
|
||||
# to the ones we've already used so far.
|
||||
#
|
||||
# Loading every time zone in the 2020a version of the time zone database
|
||||
# requires 447 timedeltas, which requires approximately the amount of space
|
||||
# that ZoneInfo("America/New_York") with 236 transitions takes up, so we will
|
||||
# set the cache size to 512 so that in the common case we always get cache
|
||||
# hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts
|
||||
# of memory.
|
||||
@functools.lru_cache(maxsize=512)
|
||||
def _load_timedelta(seconds):
|
||||
return timedelta(seconds=seconds)
|
||||
|
||||
|
||||
class ZoneInfo(tzinfo):
|
||||
_strong_cache_size = 8
|
||||
_strong_cache = collections.OrderedDict()
|
||||
_weak_cache = weakref.WeakValueDictionary()
|
||||
__module__ = "zoneinfo"
|
||||
|
||||
def __init_subclass__(cls):
|
||||
cls._strong_cache = collections.OrderedDict()
|
||||
cls._weak_cache = weakref.WeakValueDictionary()
|
||||
|
||||
def __new__(cls, key):
|
||||
instance = cls._weak_cache.get(key, None)
|
||||
if instance is None:
|
||||
instance = cls._weak_cache.setdefault(key, cls._new_instance(key))
|
||||
instance._from_cache = True
|
||||
|
||||
# Update the "strong" cache
|
||||
cls._strong_cache[key] = cls._strong_cache.pop(key, instance)
|
||||
|
||||
if len(cls._strong_cache) > cls._strong_cache_size:
|
||||
cls._strong_cache.popitem(last=False)
|
||||
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def no_cache(cls, key):
|
||||
obj = cls._new_instance(key)
|
||||
obj._from_cache = False
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def _new_instance(cls, key):
|
||||
obj = super().__new__(cls)
|
||||
obj._key = key
|
||||
obj._file_path = obj._find_tzfile(key)
|
||||
|
||||
if obj._file_path is not None:
|
||||
file_obj = open(obj._file_path, "rb")
|
||||
else:
|
||||
file_obj = _common.load_tzdata(key)
|
||||
|
||||
with file_obj as f:
|
||||
obj._load_file(f)
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, fobj, /, key=None):
|
||||
obj = super().__new__(cls)
|
||||
obj._key = key
|
||||
obj._file_path = None
|
||||
obj._load_file(fobj)
|
||||
obj._file_repr = repr(fobj)
|
||||
|
||||
# Disable pickling for objects created from files
|
||||
obj.__reduce__ = obj._file_reduce
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls, *, only_keys=None):
|
||||
if only_keys is not None:
|
||||
for key in only_keys:
|
||||
cls._weak_cache.pop(key, None)
|
||||
cls._strong_cache.pop(key, None)
|
||||
|
||||
else:
|
||||
cls._weak_cache.clear()
|
||||
cls._strong_cache.clear()
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self._find_trans(dt).utcoff
|
||||
|
||||
def dst(self, dt):
|
||||
return self._find_trans(dt).dstoff
|
||||
|
||||
def tzname(self, dt):
|
||||
return self._find_trans(dt).tzname
|
||||
|
||||
def fromutc(self, dt):
|
||||
"""Convert from datetime in UTC to datetime in local time"""
|
||||
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
timestamp = self._get_local_timestamp(dt)
|
||||
num_trans = len(self._trans_utc)
|
||||
|
||||
if num_trans >= 1 and timestamp < self._trans_utc[0]:
|
||||
tti = self._tti_before
|
||||
fold = 0
|
||||
elif (
|
||||
num_trans == 0 or timestamp > self._trans_utc[-1]
|
||||
) and not isinstance(self._tz_after, _ttinfo):
|
||||
tti, fold = self._tz_after.get_trans_info_fromutc(
|
||||
timestamp, dt.year
|
||||
)
|
||||
elif num_trans == 0:
|
||||
tti = self._tz_after
|
||||
fold = 0
|
||||
else:
|
||||
idx = bisect.bisect_right(self._trans_utc, timestamp)
|
||||
|
||||
if num_trans > 1 and timestamp >= self._trans_utc[1]:
|
||||
tti_prev, tti = self._ttinfos[idx - 2 : idx]
|
||||
elif timestamp > self._trans_utc[-1]:
|
||||
tti_prev = self._ttinfos[-1]
|
||||
tti = self._tz_after
|
||||
else:
|
||||
tti_prev = self._tti_before
|
||||
tti = self._ttinfos[0]
|
||||
|
||||
# Detect fold
|
||||
shift = tti_prev.utcoff - tti.utcoff
|
||||
fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1]
|
||||
dt += tti.utcoff
|
||||
if fold:
|
||||
return dt.replace(fold=1)
|
||||
else:
|
||||
return dt
|
||||
|
||||
def _find_trans(self, dt):
|
||||
if dt is None:
|
||||
if self._fixed_offset:
|
||||
return self._tz_after
|
||||
else:
|
||||
return _NO_TTINFO
|
||||
|
||||
ts = self._get_local_timestamp(dt)
|
||||
|
||||
lt = self._trans_local[dt.fold]
|
||||
|
||||
num_trans = len(lt)
|
||||
|
||||
if num_trans and ts < lt[0]:
|
||||
return self._tti_before
|
||||
elif not num_trans or ts > lt[-1]:
|
||||
if isinstance(self._tz_after, _TZStr):
|
||||
return self._tz_after.get_trans_info(ts, dt.year, dt.fold)
|
||||
else:
|
||||
return self._tz_after
|
||||
else:
|
||||
# idx is the transition that occurs after this timestamp, so we
|
||||
# subtract off 1 to get the current ttinfo
|
||||
idx = bisect.bisect_right(lt, ts) - 1
|
||||
assert idx >= 0
|
||||
return self._ttinfos[idx]
|
||||
|
||||
def _get_local_timestamp(self, dt):
|
||||
return (
|
||||
(dt.toordinal() - EPOCHORDINAL) * 86400
|
||||
+ dt.hour * 3600
|
||||
+ dt.minute * 60
|
||||
+ dt.second
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self._key is not None:
|
||||
return f"{self._key}"
|
||||
else:
|
||||
return repr(self)
|
||||
|
||||
def __repr__(self):
|
||||
if self._key is not None:
|
||||
return f"{self.__class__.__name__}(key={self._key!r})"
|
||||
else:
|
||||
return f"{self.__class__.__name__}.from_file({self._file_repr})"
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__._unpickle, (self._key, self._from_cache))
|
||||
|
||||
def _file_reduce(self):
|
||||
import pickle
|
||||
|
||||
raise pickle.PicklingError(
|
||||
"Cannot pickle a ZoneInfo file created from a file stream."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _unpickle(cls, key, from_cache, /):
|
||||
if from_cache:
|
||||
return cls(key)
|
||||
else:
|
||||
return cls.no_cache(key)
|
||||
|
||||
def _find_tzfile(self, key):
|
||||
return _tzpath.find_tzfile(key)
|
||||
|
||||
def _load_file(self, fobj):
|
||||
# Retrieve all the data as it exists in the zoneinfo file
|
||||
trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = _common.load_data(
|
||||
fobj
|
||||
)
|
||||
|
||||
# Infer the DST offsets (needed for .dst()) from the data
|
||||
dstoff = self._utcoff_to_dstoff(trans_idx, utcoff, isdst)
|
||||
|
||||
# Convert all the transition times (UTC) into "seconds since 1970-01-01 local time"
|
||||
trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff)
|
||||
|
||||
# Construct `_ttinfo` objects for each transition in the file
|
||||
_ttinfo_list = [
|
||||
_ttinfo(
|
||||
_load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname
|
||||
)
|
||||
for utcoffset, dstoffset, tzname in zip(utcoff, dstoff, abbr)
|
||||
]
|
||||
|
||||
self._trans_utc = trans_utc
|
||||
self._trans_local = trans_local
|
||||
self._ttinfos = [_ttinfo_list[idx] for idx in trans_idx]
|
||||
|
||||
# Find the first non-DST transition
|
||||
for i in range(len(isdst)):
|
||||
if not isdst[i]:
|
||||
self._tti_before = _ttinfo_list[i]
|
||||
break
|
||||
else:
|
||||
if self._ttinfos:
|
||||
self._tti_before = self._ttinfos[0]
|
||||
else:
|
||||
self._tti_before = None
|
||||
|
||||
# Set the "fallback" time zone
|
||||
if tz_str is not None and tz_str != b"":
|
||||
self._tz_after = _parse_tz_str(tz_str.decode())
|
||||
else:
|
||||
if not self._ttinfos and not _ttinfo_list:
|
||||
raise ValueError("No time zone information found.")
|
||||
|
||||
if self._ttinfos:
|
||||
self._tz_after = self._ttinfos[-1]
|
||||
else:
|
||||
self._tz_after = _ttinfo_list[-1]
|
||||
|
||||
# Determine if this is a "fixed offset" zone, meaning that the output
|
||||
# of the utcoffset, dst and tzname functions does not depend on the
|
||||
# specific datetime passed.
|
||||
#
|
||||
# We make three simplifying assumptions here:
|
||||
#
|
||||
# 1. If _tz_after is not a _ttinfo, it has transitions that might
|
||||
# actually occur (it is possible to construct TZ strings that
|
||||
# specify STD and DST but no transitions ever occur, such as
|
||||
# AAA0BBB,0/0,J365/25).
|
||||
# 2. If _ttinfo_list contains more than one _ttinfo object, the objects
|
||||
# represent different offsets.
|
||||
# 3. _ttinfo_list contains no unused _ttinfos (in which case an
|
||||
# otherwise fixed-offset zone with extra _ttinfos defined may
|
||||
# appear to *not* be a fixed offset zone).
|
||||
#
|
||||
# Violations to these assumptions would be fairly exotic, and exotic
|
||||
# zones should almost certainly not be used with datetime.time (the
|
||||
# only thing that would be affected by this).
|
||||
if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo):
|
||||
self._fixed_offset = False
|
||||
elif not _ttinfo_list:
|
||||
self._fixed_offset = True
|
||||
else:
|
||||
self._fixed_offset = _ttinfo_list[0] == self._tz_after
|
||||
|
||||
@staticmethod
|
||||
def _utcoff_to_dstoff(trans_idx, utcoffsets, isdsts):
|
||||
# Now we must transform our ttis and abbrs into `_ttinfo` objects,
|
||||
# but there is an issue: .dst() must return a timedelta with the
|
||||
# difference between utcoffset() and the "standard" offset, but
|
||||
# the "base offset" and "DST offset" are not encoded in the file;
|
||||
# we can infer what they are from the isdst flag, but it is not
|
||||
# sufficient to just look at the last standard offset, because
|
||||
# occasionally countries will shift both DST offset and base offset.
|
||||
|
||||
typecnt = len(isdsts)
|
||||
dstoffs = [0] * typecnt # Provisionally assign all to 0.
|
||||
dst_cnt = sum(isdsts)
|
||||
dst_found = 0
|
||||
|
||||
for i in range(1, len(trans_idx)):
|
||||
if dst_cnt == dst_found:
|
||||
break
|
||||
|
||||
idx = trans_idx[i]
|
||||
|
||||
dst = isdsts[idx]
|
||||
|
||||
# We're only going to look at daylight saving time
|
||||
if not dst:
|
||||
continue
|
||||
|
||||
# Skip any offsets that have already been assigned
|
||||
if dstoffs[idx] != 0:
|
||||
continue
|
||||
|
||||
dstoff = 0
|
||||
utcoff = utcoffsets[idx]
|
||||
|
||||
comp_idx = trans_idx[i - 1]
|
||||
|
||||
if not isdsts[comp_idx]:
|
||||
dstoff = utcoff - utcoffsets[comp_idx]
|
||||
|
||||
if not dstoff and idx < (typecnt - 1):
|
||||
comp_idx = trans_idx[i + 1]
|
||||
|
||||
# If the following transition is also DST and we couldn't
|
||||
# find the DST offset by this point, we're going to have to
|
||||
# skip it and hope this transition gets assigned later
|
||||
if isdsts[comp_idx]:
|
||||
continue
|
||||
|
||||
dstoff = utcoff - utcoffsets[comp_idx]
|
||||
|
||||
if dstoff:
|
||||
dst_found += 1
|
||||
dstoffs[idx] = dstoff
|
||||
else:
|
||||
# If we didn't find a valid value for a given index, we'll end up
|
||||
# with dstoff = 0 for something where `isdst=1`. This is obviously
|
||||
# wrong - one hour will be a much better guess than 0
|
||||
for idx in range(typecnt):
|
||||
if not dstoffs[idx] and isdsts[idx]:
|
||||
dstoffs[idx] = 3600
|
||||
|
||||
return dstoffs
|
||||
|
||||
@staticmethod
|
||||
def _ts_to_local(trans_idx, trans_list_utc, utcoffsets):
|
||||
"""Generate number of seconds since 1970 *in the local time*.
|
||||
|
||||
This is necessary to easily find the transition times in local time"""
|
||||
if not trans_list_utc:
|
||||
return [[], []]
|
||||
|
||||
# Start with the timestamps and modify in-place
|
||||
trans_list_wall = [list(trans_list_utc), list(trans_list_utc)]
|
||||
|
||||
if len(utcoffsets) > 1:
|
||||
offset_0 = utcoffsets[0]
|
||||
offset_1 = utcoffsets[trans_idx[0]]
|
||||
if offset_1 > offset_0:
|
||||
offset_1, offset_0 = offset_0, offset_1
|
||||
else:
|
||||
offset_0 = offset_1 = utcoffsets[0]
|
||||
|
||||
trans_list_wall[0][0] += offset_0
|
||||
trans_list_wall[1][0] += offset_1
|
||||
|
||||
for i in range(1, len(trans_idx)):
|
||||
offset_0 = utcoffsets[trans_idx[i - 1]]
|
||||
offset_1 = utcoffsets[trans_idx[i]]
|
||||
|
||||
if offset_1 > offset_0:
|
||||
offset_1, offset_0 = offset_0, offset_1
|
||||
|
||||
trans_list_wall[0][i] += offset_0
|
||||
trans_list_wall[1][i] += offset_1
|
||||
|
||||
return trans_list_wall
|
||||
|
||||
|
||||
class _ttinfo:
|
||||
__slots__ = ["utcoff", "dstoff", "tzname"]
|
||||
|
||||
def __init__(self, utcoff, dstoff, tzname):
|
||||
self.utcoff = utcoff
|
||||
self.dstoff = dstoff
|
||||
self.tzname = tzname
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.utcoff == other.utcoff
|
||||
and self.dstoff == other.dstoff
|
||||
and self.tzname == other.tzname
|
||||
)
|
||||
|
||||
def __repr__(self): # pragma: nocover
|
||||
return (
|
||||
f"{self.__class__.__name__}"
|
||||
+ f"({self.utcoff}, {self.dstoff}, {self.tzname})"
|
||||
)
|
||||
|
||||
|
||||
_NO_TTINFO = _ttinfo(None, None, None)
|
||||
|
||||
|
||||
class _TZStr:
|
||||
__slots__ = (
|
||||
"std",
|
||||
"dst",
|
||||
"start",
|
||||
"end",
|
||||
"get_trans_info",
|
||||
"get_trans_info_fromutc",
|
||||
"dst_diff",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None
|
||||
):
|
||||
self.dst_diff = dst_offset - std_offset
|
||||
std_offset = _load_timedelta(std_offset)
|
||||
self.std = _ttinfo(
|
||||
utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr
|
||||
)
|
||||
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
dst_offset = _load_timedelta(dst_offset)
|
||||
delta = _load_timedelta(self.dst_diff)
|
||||
self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr)
|
||||
|
||||
# These are assertions because the constructor should only be called
|
||||
# by functions that would fail before passing start or end
|
||||
assert start is not None, "No transition start specified"
|
||||
assert end is not None, "No transition end specified"
|
||||
|
||||
self.get_trans_info = self._get_trans_info
|
||||
self.get_trans_info_fromutc = self._get_trans_info_fromutc
|
||||
|
||||
def transitions(self, year):
|
||||
start = self.start.year_to_epoch(year)
|
||||
end = self.end.year_to_epoch(year)
|
||||
return start, end
|
||||
|
||||
def _get_trans_info(self, ts, year, fold):
|
||||
"""Get the information about the current transition - tti"""
|
||||
start, end = self.transitions(year)
|
||||
|
||||
# With fold = 0, the period (denominated in local time) with the
|
||||
# smaller offset starts at the end of the gap and ends at the end of
|
||||
# the fold; with fold = 1, it runs from the start of the gap to the
|
||||
# beginning of the fold.
|
||||
#
|
||||
# So in order to determine the DST boundaries we need to know both
|
||||
# the fold and whether DST is positive or negative (rare), and it
|
||||
# turns out that this boils down to fold XOR is_positive.
|
||||
if fold == (self.dst_diff >= 0):
|
||||
end -= self.dst_diff
|
||||
else:
|
||||
start += self.dst_diff
|
||||
|
||||
if start < end:
|
||||
isdst = start <= ts < end
|
||||
else:
|
||||
isdst = not (end <= ts < start)
|
||||
|
||||
return self.dst if isdst else self.std
|
||||
|
||||
def _get_trans_info_fromutc(self, ts, year):
|
||||
start, end = self.transitions(year)
|
||||
start -= self.std.utcoff.total_seconds()
|
||||
end -= self.dst.utcoff.total_seconds()
|
||||
|
||||
if start < end:
|
||||
isdst = start <= ts < end
|
||||
else:
|
||||
isdst = not (end <= ts < start)
|
||||
|
||||
# For positive DST, the ambiguous period is one dst_diff after the end
|
||||
# of DST; for negative DST, the ambiguous period is one dst_diff before
|
||||
# the start of DST.
|
||||
if self.dst_diff > 0:
|
||||
ambig_start = end
|
||||
ambig_end = end + self.dst_diff
|
||||
else:
|
||||
ambig_start = start
|
||||
ambig_end = start - self.dst_diff
|
||||
|
||||
fold = ambig_start <= ts < ambig_end
|
||||
|
||||
return (self.dst if isdst else self.std, fold)
|
||||
|
||||
|
||||
def _post_epoch_days_before_year(year):
|
||||
"""Get the number of days between 1970-01-01 and YEAR-01-01"""
|
||||
y = year - 1
|
||||
return y * 365 + y // 4 - y // 100 + y // 400 - EPOCHORDINAL
|
||||
|
||||
|
||||
class _DayOffset:
|
||||
__slots__ = ["d", "julian", "hour", "minute", "second"]
|
||||
|
||||
def __init__(self, d, julian, hour=2, minute=0, second=0):
|
||||
min_day = 0 + julian # convert bool to int
|
||||
if not min_day <= d <= 365:
|
||||
raise ValueError(f"d must be in [{min_day}, 365], not: {d}")
|
||||
|
||||
self.d = d
|
||||
self.julian = julian
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
|
||||
def year_to_epoch(self, year):
|
||||
days_before_year = _post_epoch_days_before_year(year)
|
||||
|
||||
d = self.d
|
||||
if self.julian and d >= 59 and calendar.isleap(year):
|
||||
d += 1
|
||||
|
||||
epoch = (days_before_year + d) * 86400
|
||||
epoch += self.hour * 3600 + self.minute * 60 + self.second
|
||||
|
||||
return epoch
|
||||
|
||||
|
||||
class _CalendarOffset:
|
||||
__slots__ = ["m", "w", "d", "hour", "minute", "second"]
|
||||
|
||||
_DAYS_BEFORE_MONTH = (
|
||||
-1,
|
||||
0,
|
||||
31,
|
||||
59,
|
||||
90,
|
||||
120,
|
||||
151,
|
||||
181,
|
||||
212,
|
||||
243,
|
||||
273,
|
||||
304,
|
||||
334,
|
||||
)
|
||||
|
||||
def __init__(self, m, w, d, hour=2, minute=0, second=0):
|
||||
if not 1 <= m <= 12:
|
||||
raise ValueError("m must be in [1, 12]")
|
||||
|
||||
if not 1 <= w <= 5:
|
||||
raise ValueError("w must be in [1, 5]")
|
||||
|
||||
if not 0 <= d <= 6:
|
||||
raise ValueError("d must be in [0, 6]")
|
||||
|
||||
self.m = m
|
||||
self.w = w
|
||||
self.d = d
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
|
||||
@classmethod
|
||||
def _ymd2ord(cls, year, month, day):
|
||||
return (
|
||||
_post_epoch_days_before_year(year)
|
||||
+ cls._DAYS_BEFORE_MONTH[month]
|
||||
+ (month > 2 and calendar.isleap(year))
|
||||
+ day
|
||||
)
|
||||
|
||||
# TODO: These are not actually epoch dates as they are expressed in local time
|
||||
def year_to_epoch(self, year):
|
||||
"""Calculates the datetime of the occurrence from the year"""
|
||||
# We know year and month, we need to convert w, d into day of month
|
||||
#
|
||||
# Week 1 is the first week in which day `d` (where 0 = Sunday) appears.
|
||||
# Week 5 represents the last occurrence of day `d`, so we need to know
|
||||
# the range of the month.
|
||||
first_day, days_in_month = calendar.monthrange(year, self.m)
|
||||
|
||||
# This equation seems magical, so I'll break it down:
|
||||
# 1. calendar says 0 = Monday, POSIX says 0 = Sunday
|
||||
# so we need first_day + 1 to get 1 = Monday -> 7 = Sunday,
|
||||
# which is still equivalent because this math is mod 7
|
||||
# 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need
|
||||
# to do anything to adjust negative numbers.
|
||||
# 3. Add 1 because month days are a 1-based index.
|
||||
month_day = (self.d - (first_day + 1)) % 7 + 1
|
||||
|
||||
# Now use a 0-based index version of `w` to calculate the w-th
|
||||
# occurrence of `d`
|
||||
month_day += (self.w - 1) * 7
|
||||
|
||||
# month_day will only be > days_in_month if w was 5, and `w` means
|
||||
# "last occurrence of `d`", so now we just check if we over-shot the
|
||||
# end of the month and if so knock off 1 week.
|
||||
if month_day > days_in_month:
|
||||
month_day -= 7
|
||||
|
||||
ordinal = self._ymd2ord(year, self.m, month_day)
|
||||
epoch = ordinal * 86400
|
||||
epoch += self.hour * 3600 + self.minute * 60 + self.second
|
||||
return epoch
|
||||
|
||||
|
||||
def _parse_tz_str(tz_str):
|
||||
# The tz string has the format:
|
||||
#
|
||||
# std[offset[dst[offset],start[/time],end[/time]]]
|
||||
#
|
||||
# std and dst must be 3 or more characters long and must not contain
|
||||
# a leading colon, embedded digits, commas, nor a plus or minus signs;
|
||||
# The spaces between "std" and "offset" are only for display and are
|
||||
# not actually present in the string.
|
||||
#
|
||||
# The format of the offset is ``[+|-]hh[:mm[:ss]]``
|
||||
|
||||
offset_str, *start_end_str = tz_str.split(",", 1)
|
||||
|
||||
parser_re = re.compile(
|
||||
r"""
|
||||
(?P<std>[^<0-9:.+-]+|<[a-zA-Z0-9+-]+>)
|
||||
(?:
|
||||
(?P<stdoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)
|
||||
(?:
|
||||
(?P<dst>[^0-9:.+-]+|<[a-zA-Z0-9+-]+>)
|
||||
(?P<dstoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)?
|
||||
)? # dst
|
||||
)? # stdoff
|
||||
""",
|
||||
re.ASCII|re.VERBOSE
|
||||
)
|
||||
|
||||
m = parser_re.fullmatch(offset_str)
|
||||
|
||||
if m is None:
|
||||
raise ValueError(f"{tz_str} is not a valid TZ string")
|
||||
|
||||
std_abbr = m.group("std")
|
||||
dst_abbr = m.group("dst")
|
||||
dst_offset = None
|
||||
|
||||
std_abbr = std_abbr.strip("<>")
|
||||
|
||||
if dst_abbr:
|
||||
dst_abbr = dst_abbr.strip("<>")
|
||||
|
||||
if std_offset := m.group("stdoff"):
|
||||
try:
|
||||
std_offset = _parse_tz_delta(std_offset)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid STD offset in {tz_str}") from e
|
||||
else:
|
||||
std_offset = 0
|
||||
|
||||
if dst_abbr is not None:
|
||||
if dst_offset := m.group("dstoff"):
|
||||
try:
|
||||
dst_offset = _parse_tz_delta(dst_offset)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid DST offset in {tz_str}") from e
|
||||
else:
|
||||
dst_offset = std_offset + 3600
|
||||
|
||||
if not start_end_str:
|
||||
raise ValueError(f"Missing transition rules: {tz_str}")
|
||||
|
||||
start_end_strs = start_end_str[0].split(",", 1)
|
||||
try:
|
||||
start, end = (_parse_dst_start_end(x) for x in start_end_strs)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid TZ string: {tz_str}") from e
|
||||
|
||||
return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end)
|
||||
elif start_end_str:
|
||||
raise ValueError(f"Transition rule present without DST: {tz_str}")
|
||||
else:
|
||||
# This is a static ttinfo, don't return _TZStr
|
||||
return _ttinfo(
|
||||
_load_timedelta(std_offset), _load_timedelta(0), std_abbr
|
||||
)
|
||||
|
||||
|
||||
def _parse_dst_start_end(dststr):
|
||||
date, *time = dststr.split("/", 1)
|
||||
type = date[:1]
|
||||
if type == "M":
|
||||
n_is_julian = False
|
||||
m = re.fullmatch(r"M(\d{1,2})\.(\d).(\d)", date, re.ASCII)
|
||||
if m is None:
|
||||
raise ValueError(f"Invalid dst start/end date: {dststr}")
|
||||
date_offset = tuple(map(int, m.groups()))
|
||||
offset = _CalendarOffset(*date_offset)
|
||||
else:
|
||||
if type == "J":
|
||||
n_is_julian = True
|
||||
date = date[1:]
|
||||
else:
|
||||
n_is_julian = False
|
||||
|
||||
doy = int(date)
|
||||
offset = _DayOffset(doy, n_is_julian)
|
||||
|
||||
if time:
|
||||
offset.hour, offset.minute, offset.second = _parse_transition_time(time[0])
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
def _parse_transition_time(time_str):
|
||||
match = re.fullmatch(
|
||||
r"(?P<sign>[+-])?(?P<h>\d{1,3})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
|
||||
time_str,
|
||||
re.ASCII
|
||||
)
|
||||
if match is None:
|
||||
raise ValueError(f"Invalid time: {time_str}")
|
||||
|
||||
h, m, s = (int(v or 0) for v in match.group("h", "m", "s"))
|
||||
|
||||
if h > 167:
|
||||
raise ValueError(
|
||||
f"Hour must be in [0, 167]: {time_str}"
|
||||
)
|
||||
|
||||
if match.group("sign") == "-":
|
||||
h, m, s = -h, -m, -s
|
||||
|
||||
return h, m, s
|
||||
|
||||
|
||||
def _parse_tz_delta(tz_delta):
|
||||
match = re.fullmatch(
|
||||
r"(?P<sign>[+-])?(?P<h>\d{1,3})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
|
||||
tz_delta,
|
||||
re.ASCII
|
||||
)
|
||||
# Anything passed to this function should already have hit an equivalent
|
||||
# regular expression to find the section to parse.
|
||||
assert match is not None, tz_delta
|
||||
|
||||
h, m, s = (int(v or 0) for v in match.group("h", "m", "s"))
|
||||
|
||||
total = h * 3600 + m * 60 + s
|
||||
|
||||
if h > 24:
|
||||
raise ValueError(
|
||||
f"Offset hours must be in [0, 24]: {tz_delta}"
|
||||
)
|
||||
|
||||
# Yes, +5 maps to an offset of -5h
|
||||
if match.group("sign") != "-":
|
||||
total = -total
|
||||
|
||||
return total
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-common"
|
||||
version = "0.4.0"
|
||||
description = "General python functions and algorithms for use in RustPython"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2021"
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
threading = ["parking_lot"]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-compiler"
|
||||
version = "0.4.0"
|
||||
description = "A usability wrapper around rustpython-parser and rustpython-compiler-core"
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rustpython-codegen = { workspace = true }
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
[package]
|
||||
name = "rustpython-codegen"
|
||||
version = "0.4.0"
|
||||
description = "Compiler for python code into bytecode for the rustpython VM."
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
|
||||
[dependencies]
|
||||
rustpython-ast = { workspace = true, features=["unparse", "constant-optimization"] }
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-compiler-core"
|
||||
description = "RustPython specific bytecode."
|
||||
version = "0.4.0"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2021"
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rustpython-parser-core = { workspace = true, features=["location"] }
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-derive-impl"
|
||||
version = "0.4.0"
|
||||
description = "Rust language extensions and macros specific to rustpython."
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rustpython-compiler-core = { workspace = true }
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-derive"
|
||||
version = "0.4.0"
|
||||
description = "Rust language extensions and macros specific to rustpython."
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-jit"
|
||||
version = "0.4.0"
|
||||
description = "Experimental JIT(just in time) compiler for python code."
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
autotests = false
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "rustpython-pylib"
|
||||
version = "0.4.0"
|
||||
authors = ["RustPython Team"]
|
||||
description = "A subset of the Python standard library for use with RustPython"
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license-file = "Lib/PSF-LICENSE"
|
||||
edition = "2021"
|
||||
include = ["Cargo.toml", "src/**/*.rs", "Lib/", "!Lib/**/test/", "!Lib/**/*.pyc"]
|
||||
authors = ["CPython Developers"]
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[features]
|
||||
freeze-stdlib = []
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython-stdlib"
|
||||
version = "0.4.0"
|
||||
description = "RustPython standard libraries in Rust."
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -15,13 +16,14 @@ compiler = ["rustpython-vm/compiler"]
|
||||
threading = ["rustpython-common/threading", "rustpython-vm/threading"]
|
||||
zlib = ["libz-sys", "flate2/zlib"]
|
||||
bz2 = ["bzip2"]
|
||||
sqlite = ["dep:libsqlite3-sys"]
|
||||
ssl = ["openssl", "openssl-sys", "foreign-types-shared"]
|
||||
ssl-vendor = ["ssl", "openssl/vendored", "openssl-probe"]
|
||||
|
||||
[dependencies]
|
||||
# rustpython crates
|
||||
rustpython-derive = { workspace = true }
|
||||
rustpython-vm = { workspace = true }
|
||||
rustpython-vm = { workspace = true, default-features = false }
|
||||
rustpython-common = { workspace = true }
|
||||
|
||||
ahash = { workspace = true }
|
||||
@@ -96,6 +98,9 @@ page_size = "0.4"
|
||||
[target.'cfg(all(unix, not(target_os = "redox"), not(target_os = "ios")))'.dependencies]
|
||||
termios = "0.3.3"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
rustix = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
gethostname = "0.2.3"
|
||||
socket2 = { version = "0.5.6", features = ["all"] }
|
||||
@@ -106,7 +111,7 @@ openssl-probe = { version = "0.1.5", optional = true }
|
||||
foreign-types-shared = { version = "0.1.1", optional = true }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies]
|
||||
libsqlite3-sys = { version = "0.28", features = ["bundled"] }
|
||||
libsqlite3-sys = { version = "0.28", features = ["bundled"], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
junction = { workspace = true }
|
||||
@@ -134,4 +139,4 @@ features = [
|
||||
system-configuration = "0.5.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
workspace = true
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
fn main() {
|
||||
println!(r#"cargo::rustc-check-cfg=cfg(osslconf, values("OPENSSL_NO_COMP"))"#);
|
||||
println!("cargo::rustc-check-cfg=cfg(ossl101)");
|
||||
println!("cargo::rustc-check-cfg=cfg(ossl102)");
|
||||
println!("cargo::rustc-check-cfg=cfg(ossl110)");
|
||||
println!("cargo::rustc-check-cfg=cfg(ossl110g)");
|
||||
println!("cargo::rustc-check-cfg=cfg(ossl111)");
|
||||
|
||||
#[allow(clippy::unusual_byte_groupings)]
|
||||
let ossl_vers = [
|
||||
(0x1_00_01_00_0, "ossl101"),
|
||||
(0x1_00_02_00_0, "ossl102"),
|
||||
(0x1_01_00_00_0, "ossl110"),
|
||||
(0x1_01_00_07_0, "ossl110g"),
|
||||
(0x1_01_00_08_0, "ossl110h"),
|
||||
(0x1_01_01_00_0, "ossl111"),
|
||||
(0x1_01_01_04_0, "ossl111d"),
|
||||
(0x3_00_00_00_0, "ossl300"),
|
||||
(0x3_01_00_00_0, "ossl310"),
|
||||
(0x3_02_00_00_0, "ossl320"),
|
||||
(0x3_03_00_00_0, "ossl330"),
|
||||
];
|
||||
|
||||
for (_, cfg) in ossl_vers {
|
||||
println!("cargo::rustc-check-cfg=cfg({cfg})");
|
||||
}
|
||||
|
||||
#[allow(clippy::unusual_byte_groupings)]
|
||||
if let Ok(v) = std::env::var("DEP_OPENSSL_VERSION_NUMBER") {
|
||||
println!("cargo:rustc-env=OPENSSL_API_VERSION={v}");
|
||||
// cfg setup from openssl crate's build script
|
||||
let version = u64::from_str_radix(&v, 16).unwrap();
|
||||
if version >= 0x1_00_01_00_0 {
|
||||
println!("cargo:rustc-cfg=ossl101");
|
||||
}
|
||||
if version >= 0x1_00_02_00_0 {
|
||||
println!("cargo:rustc-cfg=ossl102");
|
||||
}
|
||||
if version >= 0x1_01_00_00_0 {
|
||||
println!("cargo:rustc-cfg=ossl110");
|
||||
}
|
||||
if version >= 0x1_01_00_07_0 {
|
||||
println!("cargo:rustc-cfg=ossl110g");
|
||||
}
|
||||
if version >= 0x1_01_01_00_0 {
|
||||
println!("cargo:rustc-cfg=ossl111");
|
||||
for (ver, cfg) in ossl_vers {
|
||||
if version >= ver {
|
||||
println!("cargo:rustc-cfg={cfg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(v) = std::env::var("DEP_OPENSSL_CONF") {
|
||||
|
||||
@@ -13,18 +13,23 @@ mod decl {
|
||||
let co = if let Ok(co) = obj.get_attr("__code__", vm) {
|
||||
// Method or function:
|
||||
PyRef::try_from_object(vm, co)?
|
||||
} else if let Ok(_co_str) = PyStrRef::try_from_object(vm, obj.clone()) {
|
||||
} else if let Ok(co_str) = PyStrRef::try_from_object(vm, obj.clone()) {
|
||||
#[cfg(not(feature = "compiler"))]
|
||||
return Err(vm.new_runtime_error(
|
||||
"dis.dis() with str argument requires `compiler` feature".to_owned(),
|
||||
));
|
||||
{
|
||||
let _ = co_str;
|
||||
return Err(vm.new_runtime_error(
|
||||
"dis.dis() with str argument requires `compiler` feature".to_owned(),
|
||||
));
|
||||
}
|
||||
#[cfg(feature = "compiler")]
|
||||
vm.compile(
|
||||
_co_str.as_str(),
|
||||
crate::vm::compiler::Mode::Exec,
|
||||
"<dis>".to_owned(),
|
||||
)
|
||||
.map_err(|err| vm.new_syntax_error(&err, Some(_co_str.as_str())))?
|
||||
{
|
||||
vm.compile(
|
||||
co_str.as_str(),
|
||||
crate::vm::compiler::Mode::Exec,
|
||||
"<dis>".to_owned(),
|
||||
)
|
||||
.map_err(|err| vm.new_syntax_error(&err, Some(co_str.as_str())))?
|
||||
}
|
||||
} else {
|
||||
PyRef::try_from_object(vm, obj)?
|
||||
};
|
||||
|
||||
@@ -61,7 +61,10 @@ mod resource;
|
||||
mod scproxy;
|
||||
#[cfg(any(unix, windows, target_os = "wasi"))]
|
||||
mod select;
|
||||
#[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
|
||||
#[cfg(all(
|
||||
feature = "sqlite",
|
||||
not(any(target_os = "android", target_arch = "wasm32"))
|
||||
))]
|
||||
mod sqlite;
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "ssl"))]
|
||||
mod ssl;
|
||||
@@ -141,7 +144,7 @@ pub fn get_module_inits() -> impl Iterator<Item = (Cow<'static, str>, StdlibInit
|
||||
"_multiprocessing" => multiprocessing::make_module,
|
||||
"_socket" => socket::make_module,
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_arch = "wasm32")))]
|
||||
#[cfg(all(feature = "sqlite", not(any(target_os = "android", target_arch = "wasm32"))))]
|
||||
{
|
||||
"_sqlite3" => sqlite::make_module,
|
||||
}
|
||||
|
||||
@@ -110,11 +110,7 @@ mod math {
|
||||
|
||||
#[pyfunction]
|
||||
fn copysign(x: ArgIntoFloat, y: ArgIntoFloat) -> f64 {
|
||||
if x.is_nan() || y.is_nan() {
|
||||
x.into()
|
||||
} else {
|
||||
x.copysign(*y)
|
||||
}
|
||||
x.copysign(*y)
|
||||
}
|
||||
|
||||
// Power and logarithmic functions:
|
||||
|
||||
@@ -5,23 +5,29 @@ use crate::vm::{
|
||||
stdlib::posix,
|
||||
{PyObjectRef, PyResult, TryFromObject, VirtualMachine},
|
||||
};
|
||||
use nix::{errno::Errno, unistd};
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
use std::ffi::CStr;
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use itertools::Itertools;
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
unistd::{self, Pid},
|
||||
};
|
||||
use std::{
|
||||
convert::Infallible as Never,
|
||||
ffi::CString,
|
||||
io::{self, prelude::*},
|
||||
ffi::{CStr, CString},
|
||||
io::prelude::*,
|
||||
marker::PhantomData,
|
||||
ops::Deref,
|
||||
os::fd::FromRawFd,
|
||||
};
|
||||
use std::{fs::File, os::unix::io::AsRawFd};
|
||||
use unistd::{Gid, Uid};
|
||||
|
||||
pub(crate) use _posixsubprocess::make_module;
|
||||
|
||||
#[pymodule]
|
||||
mod _posixsubprocess {
|
||||
use super::{exec, CStrPathLike, ForkExecArgs, ProcArgs};
|
||||
use rustpython_vm::{AsObject, TryFromBorrowedObject};
|
||||
|
||||
use super::*;
|
||||
use crate::vm::{convert::IntoPyException, PyResult, VirtualMachine};
|
||||
|
||||
#[pyfunction]
|
||||
@@ -29,19 +35,20 @@ mod _posixsubprocess {
|
||||
if args.preexec_fn.is_some() {
|
||||
return Err(vm.new_not_implemented_error("preexec_fn not supported yet".to_owned()));
|
||||
}
|
||||
let cstrs_to_ptrs = |cstrs: &[CStrPathLike]| {
|
||||
cstrs
|
||||
.iter()
|
||||
.map(|s| s.s.as_ptr())
|
||||
.chain(std::iter::once(std::ptr::null()))
|
||||
.collect::<Vec<_>>()
|
||||
let extra_groups = args
|
||||
.groups_list
|
||||
.as_ref()
|
||||
.map(|l| Vec::<Gid>::try_from_borrowed_object(vm, l.as_object()))
|
||||
.transpose()?;
|
||||
let argv = CharPtrVec::from_iter(args.args.iter());
|
||||
let envp = args.env_list.as_ref().map(CharPtrVec::from_iter);
|
||||
let procargs = ProcArgs {
|
||||
argv: &argv,
|
||||
envp: envp.as_deref(),
|
||||
extra_groups: extra_groups.as_deref(),
|
||||
};
|
||||
let argv = cstrs_to_ptrs(&args.args);
|
||||
let argv = &argv;
|
||||
let envp = args.env_list.as_ref().map(|s| cstrs_to_ptrs(s));
|
||||
let envp = envp.as_deref();
|
||||
match unsafe { nix::unistd::fork() }.map_err(|err| err.into_pyexception(vm))? {
|
||||
nix::unistd::ForkResult::Child => exec(&args, ProcArgs { argv, envp }),
|
||||
nix::unistd::ForkResult::Child => exec(&args, procargs),
|
||||
nix::unistd::ForkResult::Parent { child } => Ok(child.as_raw()),
|
||||
}
|
||||
}
|
||||
@@ -49,7 +56,6 @@ mod _posixsubprocess {
|
||||
|
||||
macro_rules! gen_args {
|
||||
($($field:ident: $t:ty),*$(,)?) => {
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromArgs)]
|
||||
struct ForkExecArgs {
|
||||
$(#[pyarg(positional)] $field: $t,)*
|
||||
@@ -66,6 +72,52 @@ impl TryFromObject for CStrPathLike {
|
||||
Ok(CStrPathLike { s })
|
||||
}
|
||||
}
|
||||
impl AsRef<CStr> for CStrPathLike {
|
||||
fn as_ref(&self) -> &CStr {
|
||||
&self.s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CharPtrVec<'a> {
|
||||
vec: Vec<*const libc::c_char>,
|
||||
marker: PhantomData<Vec<&'a CStr>>,
|
||||
}
|
||||
|
||||
impl<'a, T: AsRef<CStr>> FromIterator<&'a T> for CharPtrVec<'a> {
|
||||
fn from_iter<I: IntoIterator<Item = &'a T>>(iter: I) -> Self {
|
||||
let vec = iter
|
||||
.into_iter()
|
||||
.map(|x| x.as_ref().as_ptr())
|
||||
.chain(std::iter::once(std::ptr::null()))
|
||||
.collect();
|
||||
Self {
|
||||
vec,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for CharPtrVec<'a> {
|
||||
type Target = CharPtrSlice<'a>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe {
|
||||
&*(self.vec.as_slice() as *const [*const libc::c_char] as *const CharPtrSlice<'a>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
struct CharPtrSlice<'a> {
|
||||
marker: PhantomData<[&'a CStr]>,
|
||||
slice: [*const libc::c_char],
|
||||
}
|
||||
|
||||
impl<'a> CharPtrSlice<'a> {
|
||||
fn as_ptr(&self) -> *const *const libc::c_char {
|
||||
self.slice.as_ptr()
|
||||
}
|
||||
}
|
||||
|
||||
gen_args! {
|
||||
args: ArgSequence<CStrPathLike> /* list */,
|
||||
@@ -84,39 +136,56 @@ gen_args! {
|
||||
errpipe_write: i32,
|
||||
restore_signals: bool,
|
||||
call_setsid: bool,
|
||||
// TODO: Difference between gid_to_set and gid_object.
|
||||
// One is a `gid_t` and the other is a `PyObject` in CPython.
|
||||
gid_to_set: Option<Option<Gid>>,
|
||||
gid_object: PyObjectRef,
|
||||
pgid_to_set: libc::pid_t,
|
||||
gid: Option<Gid>,
|
||||
groups_list: Option<PyListRef>,
|
||||
uid: Option<Option<Uid>>,
|
||||
uid: Option<Uid>,
|
||||
child_umask: i32,
|
||||
preexec_fn: Option<PyObjectRef>,
|
||||
use_vfork: bool,
|
||||
_use_vfork: bool,
|
||||
}
|
||||
|
||||
// can't reallocate inside of exec(), so we reallocate prior to fork() and pass this along
|
||||
struct ProcArgs<'a> {
|
||||
argv: &'a [*const libc::c_char],
|
||||
envp: Option<&'a [*const libc::c_char]>,
|
||||
argv: &'a CharPtrSlice<'a>,
|
||||
envp: Option<&'a CharPtrSlice<'a>>,
|
||||
extra_groups: Option<&'a [Gid]>,
|
||||
}
|
||||
|
||||
fn exec(args: &ForkExecArgs, procargs: ProcArgs) -> ! {
|
||||
match exec_inner(args, procargs) {
|
||||
let mut ctx = ExecErrorContext::NoExec;
|
||||
match exec_inner(args, procargs, &mut ctx) {
|
||||
Ok(x) => match x {},
|
||||
Err(e) => {
|
||||
let buf: &mut [u8] = &mut [0; 256];
|
||||
let mut cur = io::Cursor::new(&mut *buf);
|
||||
// TODO: check if reached preexec, if not then have "noexec" after
|
||||
let _ = write!(cur, "OSError:{}:", e as i32);
|
||||
let pos = cur.position();
|
||||
let _ = unistd::write(args.errpipe_write, &buf[..pos as usize]);
|
||||
let mut pipe =
|
||||
std::mem::ManuallyDrop::new(unsafe { File::from_raw_fd(args.errpipe_write) });
|
||||
let _ = write!(pipe, "OSError:{}:{}", e as i32, ctx.as_msg());
|
||||
std::process::exit(255)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_inner(args: &ForkExecArgs, procargs: ProcArgs) -> nix::Result<Never> {
|
||||
enum ExecErrorContext {
|
||||
NoExec,
|
||||
ChDir,
|
||||
Exec,
|
||||
}
|
||||
|
||||
impl ExecErrorContext {
|
||||
fn as_msg(&self) -> &'static str {
|
||||
match self {
|
||||
ExecErrorContext::NoExec => "noexec",
|
||||
ExecErrorContext::ChDir => "noexec:chdir",
|
||||
ExecErrorContext::Exec => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_inner(
|
||||
args: &ForkExecArgs,
|
||||
procargs: ProcArgs,
|
||||
ctx: &mut ExecErrorContext,
|
||||
) -> nix::Result<Never> {
|
||||
for &fd in args.fds_to_keep.as_slice() {
|
||||
if fd != args.errpipe_write {
|
||||
posix::raw_set_inheritable(fd, true)?
|
||||
@@ -158,7 +227,7 @@ fn exec_inner(args: &ForkExecArgs, procargs: ProcArgs) -> nix::Result<Never> {
|
||||
dup_into_stdio(errwrite, 2)?;
|
||||
|
||||
if let Some(ref cwd) = args.cwd {
|
||||
unistd::chdir(cwd.s.as_c_str())?
|
||||
unistd::chdir(cwd.s.as_c_str()).inspect_err(|_| *ctx = ExecErrorContext::ChDir)?
|
||||
}
|
||||
|
||||
if args.child_umask >= 0 {
|
||||
@@ -170,28 +239,35 @@ fn exec_inner(args: &ForkExecArgs, procargs: ProcArgs) -> nix::Result<Never> {
|
||||
}
|
||||
|
||||
if args.call_setsid {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
unistd::setsid()?;
|
||||
}
|
||||
|
||||
if let Some(_groups_list) = args.groups_list.as_ref() {
|
||||
// TODO: setgroups
|
||||
// unistd::setgroups(groups_size, groups);
|
||||
if args.pgid_to_set > -1 {
|
||||
unistd::setpgid(Pid::from_raw(0), Pid::from_raw(args.pgid_to_set))?;
|
||||
}
|
||||
|
||||
if let Some(_gid) = args.gid_to_set.as_ref() {
|
||||
// TODO: setgid
|
||||
// unistd::setregid(gid, gid)?;
|
||||
if let Some(_groups) = procargs.extra_groups {
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "redox")))]
|
||||
unistd::setgroups(_groups)?;
|
||||
}
|
||||
|
||||
if let Some(_uid) = args.uid.as_ref() {
|
||||
// TODO: setuid
|
||||
// unistd::setreuid(uid, uid)?;
|
||||
if let Some(gid) = args.gid.filter(|x| x.as_raw() != u32::MAX) {
|
||||
let ret = unsafe { libc::setregid(gid.as_raw(), gid.as_raw()) };
|
||||
nix::Error::result(ret)?;
|
||||
}
|
||||
|
||||
if let Some(uid) = args.uid.filter(|x| x.as_raw() != u32::MAX) {
|
||||
let ret = unsafe { libc::setreuid(uid.as_raw(), uid.as_raw()) };
|
||||
nix::Error::result(ret)?;
|
||||
}
|
||||
|
||||
*ctx = ExecErrorContext::Exec;
|
||||
|
||||
if args.close_fds {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
close_fds(3, &args.fds_to_keep)?;
|
||||
close_fds(KeepFds {
|
||||
above: 2,
|
||||
keep: &args.fds_to_keep,
|
||||
});
|
||||
}
|
||||
|
||||
let mut first_err = None;
|
||||
@@ -211,47 +287,128 @@ fn exec_inner(args: &ForkExecArgs, procargs: ProcArgs) -> nix::Result<Never> {
|
||||
Err(first_err.unwrap_or_else(Errno::last))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct KeepFds<'a> {
|
||||
above: i32,
|
||||
keep: &'a [i32],
|
||||
}
|
||||
|
||||
impl KeepFds<'_> {
|
||||
fn should_keep(self, fd: i32) -> bool {
|
||||
fd > self.above && self.keep.binary_search(&fd).is_err()
|
||||
}
|
||||
}
|
||||
|
||||
fn close_fds(keep: KeepFds<'_>) {
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
if close_dir_fds(keep).is_ok() {
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "redox")]
|
||||
if close_filetable_fds(keep).is_ok() {
|
||||
return;
|
||||
}
|
||||
close_fds_brute_force(keep)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn close_fds(above: i32, keep: &[i32]) -> nix::Result<()> {
|
||||
fn close_dir_fds(keep: KeepFds<'_>) -> nix::Result<()> {
|
||||
use nix::{dir::Dir, fcntl::OFlag};
|
||||
// TODO: close fds by brute force if readdir doesn't work:
|
||||
// https://github.com/python/cpython/blob/3.8/Modules/_posixsubprocess.c#L220
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd",
|
||||
target_vendor = "apple",
|
||||
))]
|
||||
let fd_dir_name = c"/dev/fd";
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
let fd_dir_name = c"/proc/self/fd";
|
||||
|
||||
let mut dir = Dir::open(
|
||||
FD_DIR_NAME,
|
||||
fd_dir_name,
|
||||
OFlag::O_RDONLY | OFlag::O_DIRECTORY,
|
||||
nix::sys::stat::Mode::empty(),
|
||||
)?;
|
||||
let dirfd = dir.as_raw_fd();
|
||||
for e in dir.iter() {
|
||||
if let Some(fd) = pos_int_from_ascii(e?.file_name()) {
|
||||
if fd != dirfd && fd > above && !keep.contains(&fd) {
|
||||
unistd::close(fd)?
|
||||
'outer: for e in dir.iter() {
|
||||
let e = e?;
|
||||
let mut parser = IntParser::default();
|
||||
for &c in e.file_name().to_bytes() {
|
||||
if parser.feed(c).is_err() {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
let fd = parser.num;
|
||||
if fd != dirfd && keep.should_keep(fd) {
|
||||
let _ = unistd::close(fd);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd",
|
||||
target_vendor = "apple",
|
||||
))]
|
||||
const FD_DIR_NAME: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"/dev/fd\0") };
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
const FD_DIR_NAME: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"/proc/self/fd\0") };
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn pos_int_from_ascii(name: &CStr) -> Option<i32> {
|
||||
let mut num = 0;
|
||||
for c in name.to_bytes() {
|
||||
if !c.is_ascii_digit() {
|
||||
return None;
|
||||
#[cfg(target_os = "redox")]
|
||||
fn close_filetable_fds(keep: KeepFds<'_>) -> nix::Result<()> {
|
||||
use nix::fcntl;
|
||||
use std::os::fd::{FromRawFd, OwnedFd};
|
||||
let fd = fcntl::open(
|
||||
c"/scheme/thisproc/current/filetable",
|
||||
fcntl::OFlag::O_RDONLY,
|
||||
nix::sys::stat::Mode::empty(),
|
||||
)?;
|
||||
let filetable = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
let read_one = || -> nix::Result<_> {
|
||||
let mut byte = 0;
|
||||
let n = nix::unistd::read(filetable.as_raw_fd(), std::slice::from_mut(&mut byte))?;
|
||||
Ok((n > 0).then_some(byte))
|
||||
};
|
||||
while let Some(c) = read_one()? {
|
||||
let mut parser = IntParser::default();
|
||||
if parser.feed(c).is_err() {
|
||||
continue;
|
||||
}
|
||||
let done = loop {
|
||||
let Some(c) = read_one()? else { break true };
|
||||
if parser.feed(c).is_err() {
|
||||
break false;
|
||||
}
|
||||
};
|
||||
|
||||
let fd = parser.num as i32;
|
||||
if fd != filetable.as_raw_fd() && keep.should_keep(fd) {
|
||||
let _ = unistd::close(fd);
|
||||
}
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
num = num * 10 + i32::from(c - b'0')
|
||||
}
|
||||
Some(num)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_fds_brute_force(keep: KeepFds<'_>) {
|
||||
let max_fd = nix::unistd::sysconf(nix::unistd::SysconfVar::OPEN_MAX)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(256) as i32;
|
||||
let fds = itertools::chain![Some(keep.above), keep.keep.iter().copied(), Some(max_fd)];
|
||||
for fd in fds.tuple_windows().flat_map(|(start, end)| start + 1..end) {
|
||||
let _ = unistd::close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct IntParser {
|
||||
num: i32,
|
||||
}
|
||||
|
||||
struct NonDigit;
|
||||
impl IntParser {
|
||||
fn feed(&mut self, c: u8) -> Result<(), NonDigit> {
|
||||
let digit = (c as char).to_digit(10).ok_or(NonDigit)?;
|
||||
self.num *= 10;
|
||||
self.num += digit as i32;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,12 +325,59 @@ mod decl {
|
||||
pub(super) mod poll {
|
||||
use super::*;
|
||||
use crate::vm::{
|
||||
builtins::PyFloat, common::lock::PyMutex, convert::ToPyObject, function::OptionalArg,
|
||||
stdlib::io::Fildes, AsObject, PyPayload,
|
||||
builtins::PyFloat,
|
||||
common::lock::PyMutex,
|
||||
convert::{IntoPyException, ToPyObject},
|
||||
function::OptionalArg,
|
||||
stdlib::io::Fildes,
|
||||
AsObject, PyPayload,
|
||||
};
|
||||
use libc::pollfd;
|
||||
use num_traits::ToPrimitive;
|
||||
use std::time;
|
||||
use num_traits::{Signed, ToPrimitive};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct TimeoutArg<const MILLIS: bool>(pub Option<Duration>);
|
||||
|
||||
impl<const MILLIS: bool> TryFromObject for TimeoutArg<MILLIS> {
|
||||
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
|
||||
let timeout = if vm.is_none(&obj) {
|
||||
None
|
||||
} else if let Some(float) = obj.payload::<PyFloat>() {
|
||||
let float = float.to_f64();
|
||||
if float.is_nan() {
|
||||
return Err(
|
||||
vm.new_value_error("Invalid value NaN (not a number)".to_owned())
|
||||
);
|
||||
}
|
||||
if float.is_sign_negative() {
|
||||
None
|
||||
} else {
|
||||
let secs = if MILLIS { float * 1000.0 } else { float };
|
||||
Some(Duration::from_secs_f64(secs))
|
||||
}
|
||||
} else if let Some(int) = obj.try_index_opt(vm).transpose()? {
|
||||
if int.as_bigint().is_negative() {
|
||||
None
|
||||
} else {
|
||||
let n = int.as_bigint().to_u64().ok_or_else(|| {
|
||||
vm.new_overflow_error("value out of range".to_owned())
|
||||
})?;
|
||||
Some(if MILLIS {
|
||||
Duration::from_millis(n)
|
||||
} else {
|
||||
Duration::from_secs(n)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"expected an int or float for duration, got {}",
|
||||
obj.class()
|
||||
)));
|
||||
};
|
||||
Ok(Self(timeout))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(module = "select", name = "poll")]
|
||||
#[derive(Default, Debug, PyPayload)]
|
||||
@@ -399,50 +446,31 @@ mod decl {
|
||||
#[pymethod]
|
||||
fn poll(
|
||||
&self,
|
||||
timeout: OptionalOption,
|
||||
timeout: OptionalArg<TimeoutArg<true>>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<Vec<PyObjectRef>> {
|
||||
let mut fds = self.fds.lock();
|
||||
let timeout_ms = match timeout.flatten() {
|
||||
Some(ms) => {
|
||||
let ms = if let Some(float) = ms.payload::<PyFloat>() {
|
||||
float.to_f64().to_i32()
|
||||
} else if let Some(int) = ms.try_index_opt(vm) {
|
||||
int?.as_bigint().to_i32()
|
||||
} else {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"expected an int or float for duration, got {}",
|
||||
ms.class()
|
||||
)));
|
||||
};
|
||||
ms.ok_or_else(|| vm.new_value_error("value out of range".to_owned()))?
|
||||
}
|
||||
None => -1,
|
||||
let TimeoutArg(timeout) = timeout.unwrap_or_default();
|
||||
let timeout_ms = match timeout {
|
||||
Some(d) => i32::try_from(d.as_millis())
|
||||
.map_err(|_| vm.new_overflow_error("value out of range".to_owned()))?,
|
||||
None => -1i32,
|
||||
};
|
||||
let timeout_ms = if timeout_ms < 0 { -1 } else { timeout_ms };
|
||||
let deadline = (timeout_ms >= 0)
|
||||
.then(|| time::Instant::now() + time::Duration::from_millis(timeout_ms as u64));
|
||||
let deadline = timeout.map(|d| Instant::now() + d);
|
||||
let mut poll_timeout = timeout_ms;
|
||||
loop {
|
||||
let res = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, poll_timeout) };
|
||||
let res = if res < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
match res {
|
||||
Ok(()) => break,
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => {
|
||||
vm.check_signals()?;
|
||||
if let Some(d) = deadline {
|
||||
match d.checked_duration_since(time::Instant::now()) {
|
||||
Some(remaining) => poll_timeout = remaining.as_millis() as i32,
|
||||
// we've timed out
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
match nix::Error::result(res) {
|
||||
Ok(_) => break,
|
||||
Err(nix::Error::EINTR) => vm.check_signals()?,
|
||||
Err(e) => return Err(e.into_pyexception(vm)),
|
||||
}
|
||||
if let Some(d) = deadline {
|
||||
if let Some(remaining) = d.checked_duration_since(Instant::now()) {
|
||||
poll_timeout = remaining.as_millis() as i32;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e.to_pyexception(vm)),
|
||||
}
|
||||
}
|
||||
Ok(fds
|
||||
@@ -453,4 +481,216 @@ mod decl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))]
|
||||
#[pyattr(name = "epoll", once)]
|
||||
fn epoll(vm: &VirtualMachine) -> PyTypeRef {
|
||||
use crate::vm::class::PyClassImpl;
|
||||
epoll::PyEpoll::make_class(&vm.ctx)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))]
|
||||
#[pyattr]
|
||||
use libc::{
|
||||
EPOLLERR, EPOLLEXCLUSIVE, EPOLLHUP, EPOLLIN, EPOLLMSG, EPOLLONESHOT, EPOLLOUT, EPOLLPRI,
|
||||
EPOLLRDBAND, EPOLLRDHUP, EPOLLRDNORM, EPOLLWAKEUP, EPOLLWRBAND, EPOLLWRNORM, EPOLL_CLOEXEC,
|
||||
};
|
||||
#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))]
|
||||
#[pyattr]
|
||||
const EPOLLET: u32 = libc::EPOLLET as u32;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))]
|
||||
pub(super) mod epoll {
|
||||
use super::*;
|
||||
use crate::vm::{
|
||||
builtins::PyTypeRef,
|
||||
common::lock::{PyRwLock, PyRwLockReadGuard},
|
||||
convert::{IntoPyException, ToPyObject},
|
||||
function::OptionalArg,
|
||||
stdlib::io::Fildes,
|
||||
types::Constructor,
|
||||
PyPayload,
|
||||
};
|
||||
use rustix::event::epoll::{self, EventData, EventFlags};
|
||||
use std::ops::Deref;
|
||||
use std::os::fd::{AsRawFd, IntoRawFd, OwnedFd};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[pyclass(module = "select", name = "epoll")]
|
||||
#[derive(Debug, rustpython_vm::PyPayload)]
|
||||
pub struct PyEpoll {
|
||||
epoll_fd: PyRwLock<Option<OwnedFd>>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
pub struct EpollNewArgs {
|
||||
#[pyarg(any, default = "-1")]
|
||||
sizehint: i32,
|
||||
#[pyarg(any, default = "0")]
|
||||
flags: i32,
|
||||
}
|
||||
|
||||
impl Constructor for PyEpoll {
|
||||
type Args = EpollNewArgs;
|
||||
fn py_new(cls: PyTypeRef, args: EpollNewArgs, vm: &VirtualMachine) -> PyResult {
|
||||
if let ..=-2 | 0 = args.sizehint {
|
||||
return Err(vm.new_value_error("negative sizehint".to_owned()));
|
||||
}
|
||||
if !matches!(args.flags, 0 | libc::EPOLL_CLOEXEC) {
|
||||
return Err(vm.new_os_error("invalid flags".to_owned()));
|
||||
}
|
||||
Self::new()
|
||||
.map_err(|e| e.into_pyexception(vm))?
|
||||
.into_ref_with_type(vm, cls)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
struct EpollPollArgs {
|
||||
#[pyarg(any, default)]
|
||||
timeout: poll::TimeoutArg<false>,
|
||||
#[pyarg(any, default = "-1")]
|
||||
maxevents: i32,
|
||||
}
|
||||
|
||||
#[pyclass(with(Constructor))]
|
||||
impl PyEpoll {
|
||||
fn new() -> std::io::Result<Self> {
|
||||
let epoll_fd = epoll::create(epoll::CreateFlags::CLOEXEC)?;
|
||||
let epoll_fd = Some(epoll_fd).into();
|
||||
Ok(PyEpoll { epoll_fd })
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn close(&self) -> std::io::Result<()> {
|
||||
let fd = self.epoll_fd.write().take();
|
||||
if let Some(fd) = fd {
|
||||
nix::unistd::close(fd.into_raw_fd())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn closed(&self) -> bool {
|
||||
self.epoll_fd.read().is_none()
|
||||
}
|
||||
|
||||
fn get_epoll(
|
||||
&self,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<impl Deref<Target = OwnedFd> + '_> {
|
||||
PyRwLockReadGuard::try_map(self.epoll_fd.read(), |x| x.as_ref()).map_err(|_| {
|
||||
vm.new_value_error("I/O operation on closed epoll object".to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn fileno(&self, vm: &VirtualMachine) -> PyResult<i32> {
|
||||
self.get_epoll(vm).map(|epoll_fd| epoll_fd.as_raw_fd())
|
||||
}
|
||||
|
||||
#[pyclassmethod]
|
||||
fn fromfd(cls: PyTypeRef, fd: OwnedFd, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
|
||||
let epoll_fd = Some(fd).into();
|
||||
Self { epoll_fd }.into_ref_with_type(vm, cls)
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn register(
|
||||
&self,
|
||||
fd: Fildes,
|
||||
eventmask: OptionalArg<u32>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let events = match eventmask {
|
||||
OptionalArg::Present(mask) => EventFlags::from_bits_retain(mask),
|
||||
OptionalArg::Missing => EventFlags::IN | EventFlags::PRI | EventFlags::OUT,
|
||||
};
|
||||
let epoll_fd = &*self.get_epoll(vm)?;
|
||||
let data = EventData::new_u64(fd.as_raw_fd() as u64);
|
||||
epoll::add(epoll_fd, fd, data, events).map_err(|e| e.into_pyexception(vm))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn modify(&self, fd: Fildes, eventmask: u32, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let events = EventFlags::from_bits_retain(eventmask);
|
||||
let epoll_fd = &*self.get_epoll(vm)?;
|
||||
let data = EventData::new_u64(fd.as_raw_fd() as u64);
|
||||
epoll::modify(epoll_fd, fd, data, events).map_err(|e| e.into_pyexception(vm))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn unregister(&self, fd: Fildes, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let epoll_fd = &*self.get_epoll(vm)?;
|
||||
epoll::delete(epoll_fd, fd).map_err(|e| e.into_pyexception(vm))
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn poll(&self, args: EpollPollArgs, vm: &VirtualMachine) -> PyResult<PyListRef> {
|
||||
let poll::TimeoutArg(timeout) = args.timeout;
|
||||
let maxevents = args.maxevents;
|
||||
|
||||
let make_poll_timeout = |d: Duration| i32::try_from(d.as_millis());
|
||||
let mut poll_timeout = match timeout {
|
||||
Some(d) => make_poll_timeout(d)
|
||||
.map_err(|_| vm.new_overflow_error("timeout is too large".to_owned()))?,
|
||||
None => -1,
|
||||
};
|
||||
|
||||
let deadline = timeout.map(|d| Instant::now() + d);
|
||||
let maxevents = match maxevents {
|
||||
..-1 => {
|
||||
return Err(vm.new_value_error(format!(
|
||||
"maxevents must be greater than 0, got {maxevents}"
|
||||
)))
|
||||
}
|
||||
-1 => libc::FD_SETSIZE - 1,
|
||||
_ => maxevents as usize,
|
||||
};
|
||||
|
||||
let mut events = epoll::EventVec::with_capacity(maxevents);
|
||||
|
||||
let epoll = &*self.get_epoll(vm)?;
|
||||
|
||||
loop {
|
||||
match epoll::wait(epoll, &mut events, poll_timeout) {
|
||||
Ok(()) => break,
|
||||
Err(rustix::io::Errno::INTR) => vm.check_signals()?,
|
||||
Err(e) => return Err(e.into_pyexception(vm)),
|
||||
}
|
||||
if let Some(deadline) = deadline {
|
||||
if let Some(new_timeout) = deadline.checked_duration_since(Instant::now()) {
|
||||
poll_timeout = make_poll_timeout(new_timeout).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ret = events
|
||||
.iter()
|
||||
.map(|ev| (ev.data.u64() as i32, { ev.flags }.bits()).to_pyobject(vm))
|
||||
.collect();
|
||||
|
||||
Ok(vm.ctx.new_list(ret))
|
||||
}
|
||||
|
||||
#[pymethod(magic)]
|
||||
fn enter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
|
||||
zelf.get_epoll(vm)?;
|
||||
Ok(zelf)
|
||||
}
|
||||
|
||||
#[pymethod(magic)]
|
||||
fn exit(
|
||||
&self,
|
||||
_exc_type: OptionalArg,
|
||||
_exc_value: OptionalArg,
|
||||
_exc_tb: OptionalArg,
|
||||
) -> std::io::Result<()> {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,12 +700,14 @@ mod _ssl {
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<Vec<PyObjectRef>> {
|
||||
let binary_form = binary_form.unwrap_or(false);
|
||||
let certs = self
|
||||
.ctx()
|
||||
.cert_store()
|
||||
.all_certificates()
|
||||
.iter()
|
||||
.map(|cert| cert_to_py(vm, cert, binary_form))
|
||||
let ctx = self.ctx();
|
||||
#[cfg(ossl300)]
|
||||
let certs = ctx.cert_store().all_certificates();
|
||||
#[cfg(not(ossl300))]
|
||||
let certs = ctx.cert_store().objects().iter().filter_map(|x| x.x509());
|
||||
let certs = certs
|
||||
.into_iter()
|
||||
.map(|ref cert| cert_to_py(vm, cert, binary_form))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
[package]
|
||||
name = "rustpython-vm"
|
||||
version = "0.4.0"
|
||||
description = "RustPython virtual machine."
|
||||
authors = ["RustPython Team"]
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
include = ["src/**/*.rs", "Cargo.toml", "build.rs", "Lib/**/*.py"]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["compiler"]
|
||||
default = ["compiler", "wasmbind"]
|
||||
importlib = []
|
||||
encodings = ["importlib"]
|
||||
vm-tracing-logging = []
|
||||
@@ -22,6 +23,7 @@ ast = ["rustpython-ast"]
|
||||
codegen = ["rustpython-codegen", "ast"]
|
||||
parser = ["rustpython-parser", "ast"]
|
||||
serde = ["dep:serde"]
|
||||
wasmbind = ["chrono/wasmbind", "getrandom/js", "wasm-bindgen"]
|
||||
|
||||
[dependencies]
|
||||
rustpython-compiler = { workspace = true, optional = true }
|
||||
@@ -47,6 +49,7 @@ cfg-if = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
chrono = { workspace = true, features = ["wasmbind"] }
|
||||
flame = { workspace = true, optional = true }
|
||||
getrandom = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
@@ -70,7 +73,6 @@ thread_local = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
|
||||
caseless = "0.2.1"
|
||||
getrandom = { version = "0.2.12", features = ["js"] }
|
||||
flamer = { version = "0.4", optional = true }
|
||||
half = "1.8.2"
|
||||
memoffset = "0.9.1"
|
||||
@@ -89,6 +91,7 @@ unic-ucd-category = "0.9.0"
|
||||
unic-ucd-ident = "0.9.0"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
rustix = { workspace = true }
|
||||
exitcode = "1.1.2"
|
||||
uname = "0.1.1"
|
||||
strum = "0.24.0"
|
||||
@@ -139,8 +142,9 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2.92"
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
getrandom = { workspace = true, features = ["custom"] }
|
||||
|
||||
[build-dependencies]
|
||||
glob = { workspace = true }
|
||||
|
||||
@@ -38,6 +38,9 @@ pub struct PyFunction {
|
||||
type_params: PyMutex<PyTupleRef>,
|
||||
#[cfg(feature = "jit")]
|
||||
jitted_code: OnceCell<CompiledCode>,
|
||||
annotations: PyMutex<PyDictRef>,
|
||||
module: PyMutex<PyObjectRef>,
|
||||
doc: PyMutex<PyObjectRef>,
|
||||
}
|
||||
|
||||
unsafe impl Traverse for PyFunction {
|
||||
@@ -49,6 +52,7 @@ unsafe impl Traverse for PyFunction {
|
||||
}
|
||||
|
||||
impl PyFunction {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
code: PyRef<PyCode>,
|
||||
globals: PyDictRef,
|
||||
@@ -57,6 +61,9 @@ impl PyFunction {
|
||||
kw_only_defaults: Option<PyDictRef>,
|
||||
qualname: PyStrRef,
|
||||
type_params: PyTupleRef,
|
||||
annotations: PyDictRef,
|
||||
module: PyObjectRef,
|
||||
doc: PyObjectRef,
|
||||
) -> Self {
|
||||
let name = PyMutex::new(code.obj_name.to_owned());
|
||||
PyFunction {
|
||||
@@ -69,6 +76,9 @@ impl PyFunction {
|
||||
type_params: PyMutex::new(type_params),
|
||||
#[cfg(feature = "jit")]
|
||||
jitted_code: OnceCell::new(),
|
||||
annotations: PyMutex::new(annotations),
|
||||
module: PyMutex::new(module),
|
||||
doc: PyMutex::new(doc),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +416,41 @@ impl PyFunction {
|
||||
*self.name.lock() = name;
|
||||
}
|
||||
|
||||
#[pymember(magic)]
|
||||
fn doc(_vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult {
|
||||
let zelf: PyRef<PyFunction> = zelf.downcast().unwrap_or_else(|_| unreachable!());
|
||||
let doc = zelf.doc.lock();
|
||||
Ok(doc.clone())
|
||||
}
|
||||
|
||||
#[pymember(magic, setter)]
|
||||
fn set_doc(vm: &VirtualMachine, zelf: PyObjectRef, value: PySetterValue) -> PyResult<()> {
|
||||
let zelf: PyRef<PyFunction> = zelf.downcast().unwrap_or_else(|_| unreachable!());
|
||||
let value = value.unwrap_or_none(vm);
|
||||
*zelf.doc.lock() = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pygetset(magic)]
|
||||
fn module(&self) -> PyObjectRef {
|
||||
self.module.lock().clone()
|
||||
}
|
||||
|
||||
#[pygetset(magic, setter)]
|
||||
fn set_module(&self, module: PySetterValue<PyObjectRef>, vm: &VirtualMachine) {
|
||||
*self.module.lock() = module.unwrap_or_none(vm);
|
||||
}
|
||||
|
||||
#[pygetset(magic)]
|
||||
fn annotations(&self) -> PyDictRef {
|
||||
self.annotations.lock().clone()
|
||||
}
|
||||
|
||||
#[pygetset(magic, setter)]
|
||||
fn set_annotations(&self, annotations: PyDictRef) {
|
||||
*self.annotations.lock() = annotations
|
||||
}
|
||||
|
||||
#[pygetset(magic)]
|
||||
fn qualname(&self) -> PyStrRef {
|
||||
self.qualname.lock().clone()
|
||||
|
||||
@@ -391,16 +391,10 @@ impl PySetInner {
|
||||
others: impl std::iter::Iterator<Item = ArgIterable>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let mut temp_inner = self.copy();
|
||||
let temp_inner = self.fold_op(others, PySetInner::intersection, vm)?;
|
||||
self.clear();
|
||||
for iterable in others {
|
||||
for item in iterable.iter(vm)? {
|
||||
let obj = item?;
|
||||
if temp_inner.contains(&obj, vm)? {
|
||||
self.add(obj, vm)?;
|
||||
}
|
||||
}
|
||||
temp_inner = self.copy()
|
||||
for obj in temp_inner.elements() {
|
||||
self.add(obj, vm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -442,12 +436,12 @@ impl PySetInner {
|
||||
((h ^ 89869747) ^ (h.wrapping_shl(16))).wrapping_mul(3644798167)
|
||||
}
|
||||
// Factor in the number of active entries
|
||||
let mut hash: u64 = (self.elements().len() as u64 + 1).wrapping_mul(1927868237);
|
||||
let mut hash: u64 = (self.len() as u64 + 1).wrapping_mul(1927868237);
|
||||
// Xor-in shuffled bits from every entry's hash field because xor is
|
||||
// commutative and a frozenset hash should be independent of order.
|
||||
for element in self.elements().iter() {
|
||||
hash ^= _shuffle_bits(element.hash(vm)? as u64);
|
||||
}
|
||||
hash = self.content.try_fold_keys(hash, |h, element| {
|
||||
Ok(h ^ _shuffle_bits(element.hash(vm)? as u64))
|
||||
})?;
|
||||
// Disperse patterns arising in nested frozensets
|
||||
hash ^= (hash >> 11) ^ (hash >> 25);
|
||||
hash = hash.wrapping_mul(69069).wrapping_add(907133923);
|
||||
|
||||
@@ -530,6 +530,17 @@ impl<T: Clone> Dict<T> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn try_fold_keys<Acc, Fold>(&self, init: Acc, f: Fold) -> PyResult<Acc>
|
||||
where
|
||||
Fold: FnMut(Acc, &PyObject) -> PyResult<Acc>,
|
||||
{
|
||||
self.read()
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|v| v.as_ref().map(|v| v.key.as_object()))
|
||||
.try_fold(init, f)
|
||||
}
|
||||
|
||||
/// Lookup the index for the given key.
|
||||
#[cfg_attr(feature = "flame-it", flame("Dict"))]
|
||||
fn lookup<K: DictKey + ?Sized>(
|
||||
|
||||
@@ -44,7 +44,7 @@ impl PyPayload for PyBaseException {
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
// Why `impl VirtualMachine?
|
||||
// Why `impl VirtualMachine`?
|
||||
// These functions are natively free function in CPython - not methods of PyException
|
||||
|
||||
/// Print exception chain by calling sys.excepthook
|
||||
@@ -1139,7 +1139,8 @@ pub(crate) fn errno_to_exc_type(errno: i32, vm: &VirtualMachine) -> Option<&'sta
|
||||
use crate::stdlib::errno::errors;
|
||||
let excs = &vm.ctx.exceptions;
|
||||
match errno {
|
||||
errors::EWOULDBLOCK => Some(excs.blocking_io_error),
|
||||
#[allow(unreachable_patterns)] // EAGAIN is sometimes the same as EWOULDBLOCK
|
||||
errors::EWOULDBLOCK | errors::EAGAIN => Some(excs.blocking_io_error),
|
||||
errors::EALREADY => Some(excs.blocking_io_error),
|
||||
errors::EINPROGRESS => Some(excs.blocking_io_error),
|
||||
errors::EPIPE => Some(excs.broken_pipe_error),
|
||||
|
||||
@@ -1731,6 +1731,8 @@ impl ExecutingFrame<'_> {
|
||||
None
|
||||
};
|
||||
|
||||
let module = vm.unwrap_or_none(self.globals.get_item_opt(identifier!(vm, __name__), vm)?);
|
||||
|
||||
// pop argc arguments
|
||||
// argument: name, args, globals
|
||||
// let scope = self.scope.clone();
|
||||
@@ -1742,17 +1744,16 @@ impl ExecutingFrame<'_> {
|
||||
kw_only_defaults,
|
||||
qualified_name.clone(),
|
||||
type_params,
|
||||
annotations.downcast().unwrap(),
|
||||
module,
|
||||
vm.ctx.none(),
|
||||
)
|
||||
.into_pyobject(vm);
|
||||
|
||||
func_obj.set_attr(identifier!(vm, __doc__), vm.ctx.none(), vm)?;
|
||||
|
||||
let name = qualified_name.as_str().split('.').next_back().unwrap();
|
||||
func_obj.set_attr(identifier!(vm, __name__), vm.new_pyobj(name), vm)?;
|
||||
func_obj.set_attr(identifier!(vm, __qualname__), qualified_name, vm)?;
|
||||
let module = vm.unwrap_or_none(self.globals.get_item_opt(identifier!(vm, __name__), vm)?);
|
||||
func_obj.set_attr(identifier!(vm, __module__), module, vm)?;
|
||||
func_obj.set_attr(identifier!(vm, __annotations__), annotations, vm)?;
|
||||
func_obj.set_attr(identifier!(vm, __doc__), vm.ctx.none(), vm)?;
|
||||
|
||||
self.push_value(func_obj);
|
||||
Ok(None)
|
||||
|
||||
@@ -224,6 +224,21 @@ impl<T> std::ops::Deref for ArgSequence<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a ArgSequence<T> {
|
||||
type Item = &'a T;
|
||||
type IntoIter = std::slice::Iter<'a, T>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
impl<T> IntoIterator for ArgSequence<T> {
|
||||
type Item = T;
|
||||
type IntoIter = std::vec::IntoIter<T>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TryFromObject> TryFromObject for ArgSequence<T> {
|
||||
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
|
||||
obj.try_to_value(vm).map(Self)
|
||||
|
||||
@@ -63,13 +63,12 @@ impl PyObject {
|
||||
} else if let Some(i) = self.to_number().int(vm).or_else(|| self.try_index_opt(vm)) {
|
||||
i
|
||||
} else if let Ok(Some(f)) = vm.get_special_method(self, identifier!(vm, __trunc__)) {
|
||||
// TODO: Deprecate in 3.11
|
||||
// warnings::warn(
|
||||
// vm.ctx.exceptions.deprecation_warning.clone(),
|
||||
// "The delegation of int() to __trunc__ is deprecated.".to_owned(),
|
||||
// 1,
|
||||
// vm,
|
||||
// )?;
|
||||
warnings::warn(
|
||||
vm.ctx.exceptions.deprecation_warning,
|
||||
"The delegation of int() to __trunc__ is deprecated.".to_owned(),
|
||||
1,
|
||||
vm,
|
||||
)?;
|
||||
let ret = f.invoke((), vm)?;
|
||||
ret.try_index(vm).map_err(|_| {
|
||||
vm.new_type_error(format!(
|
||||
|
||||
@@ -68,7 +68,6 @@ pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef<PyModule> {
|
||||
}
|
||||
|
||||
// not used on all platforms
|
||||
#[allow(unused)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Fildes(pub i32);
|
||||
@@ -100,6 +99,21 @@ impl TryFromObject for Fildes {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl std::os::fd::AsFd for Fildes {
|
||||
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
|
||||
// SAFETY: none, really. but, python's os api of passing around file descriptors
|
||||
// everywhere isn't really io-safe anyway, so, this is passed to the user.
|
||||
unsafe { std::os::fd::BorrowedFd::borrow_raw(self.0) }
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
impl std::os::fd::AsRawFd for Fildes {
|
||||
fn as_raw_fd(&self) -> std::os::fd::RawFd {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
mod _io {
|
||||
use super::*;
|
||||
|
||||
@@ -25,6 +25,13 @@ impl crate::convert::IntoPyException for nix::Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl crate::convert::IntoPyException for rustix::io::Errno {
|
||||
fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
|
||||
io::Error::from(self).into_pyexception(vm)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the error stored in the `errno` variable into an Exception
|
||||
#[inline]
|
||||
pub fn errno_err(vm: &VirtualMachine) -> PyBaseExceptionRef {
|
||||
|
||||
@@ -41,7 +41,7 @@ pub mod module {
|
||||
env,
|
||||
ffi::{CStr, CString},
|
||||
fs, io,
|
||||
os::fd::{AsRawFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd},
|
||||
os::fd::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd},
|
||||
};
|
||||
use strum_macros::{EnumIter, EnumString};
|
||||
|
||||
@@ -174,6 +174,18 @@ pub mod module {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromObject for OwnedFd {
|
||||
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
|
||||
let fd = i32::try_from_object(vm, obj)?;
|
||||
if fd == -1 {
|
||||
return Err(io::Error::from_raw_os_error(libc::EBADF).into_pyexception(vm));
|
||||
}
|
||||
// SAFETY: none, really. but, python's os api of passing around file descriptors
|
||||
// everywhere isn't really io-safe anyway, so, this is passed to the user.
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPyObject for OwnedFd {
|
||||
fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
|
||||
self.into_raw_fd().to_pyobject(vm)
|
||||
@@ -1111,17 +1123,13 @@ pub mod module {
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn setgid(gid: Option<Gid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let gid =
|
||||
gid.ok_or_else(|| vm.new_errno_error(1, "Operation not permitted".to_string()))?;
|
||||
fn setgid(gid: Gid, vm: &VirtualMachine) -> PyResult<()> {
|
||||
unistd::setgid(gid).map_err(|err| err.into_pyexception(vm))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
#[pyfunction]
|
||||
fn setegid(egid: Option<Gid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let egid =
|
||||
egid.ok_or_else(|| vm.new_errno_error(1, "Operation not permitted".to_string()))?;
|
||||
fn setegid(egid: Gid, vm: &VirtualMachine) -> PyResult<()> {
|
||||
unistd::setegid(egid).map_err(|err| err.into_pyexception(vm))
|
||||
}
|
||||
|
||||
@@ -1139,7 +1147,7 @@ pub mod module {
|
||||
.map_err(|err| err.into_pyexception(vm))
|
||||
}
|
||||
|
||||
fn try_from_id(vm: &VirtualMachine, obj: PyObjectRef, typ_name: &str) -> PyResult<Option<u32>> {
|
||||
fn try_from_id(vm: &VirtualMachine, obj: PyObjectRef, typ_name: &str) -> PyResult<u32> {
|
||||
use std::cmp::Ordering;
|
||||
let i = obj
|
||||
.try_to_ref::<PyInt>(vm)
|
||||
@@ -1152,53 +1160,47 @@ pub mod module {
|
||||
.try_to_primitive::<i64>(vm)?;
|
||||
|
||||
match i.cmp(&-1) {
|
||||
Ordering::Greater => Ok(Some(i.try_into().map_err(|_| {
|
||||
Ordering::Greater => Ok(i.try_into().map_err(|_| {
|
||||
vm.new_overflow_error(format!("{typ_name} is larger than maximum"))
|
||||
})?)),
|
||||
})?),
|
||||
Ordering::Less => {
|
||||
Err(vm.new_overflow_error(format!("{typ_name} is less than minimum")))
|
||||
}
|
||||
Ordering::Equal => Ok(None), // -1 means does not change the value
|
||||
// -1 means does not change the value
|
||||
// In CPython, this is `(uid_t) -1`, rustc gets mad when we try to declare
|
||||
// a negative unsigned integer :).
|
||||
Ordering::Equal => Ok(-1i32 as u32),
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromObject for Option<Uid> {
|
||||
impl TryFromObject for Uid {
|
||||
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
|
||||
Ok(try_from_id(vm, obj, "uid")?.map(Uid::from_raw))
|
||||
try_from_id(vm, obj, "uid").map(Uid::from_raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromObject for Option<Gid> {
|
||||
impl TryFromObject for Gid {
|
||||
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
|
||||
Ok(try_from_id(vm, obj, "gid")?.map(Gid::from_raw))
|
||||
try_from_id(vm, obj, "gid").map(Gid::from_raw)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn setuid(uid: Option<Uid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let uid =
|
||||
uid.ok_or_else(|| vm.new_errno_error(1, "Operation not permitted".to_string()))?;
|
||||
unistd::setuid(uid).map_err(|err| err.into_pyexception(vm))
|
||||
fn setuid(uid: Uid) -> nix::Result<()> {
|
||||
unistd::setuid(uid)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
#[pyfunction]
|
||||
fn seteuid(euid: Option<Uid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let euid =
|
||||
euid.ok_or_else(|| vm.new_errno_error(1, "Operation not permitted".to_string()))?;
|
||||
unistd::seteuid(euid).map_err(|err| err.into_pyexception(vm))
|
||||
fn seteuid(euid: Uid) -> nix::Result<()> {
|
||||
unistd::seteuid(euid)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
#[pyfunction]
|
||||
fn setreuid(ruid: Option<Uid>, euid: Option<Uid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
if let Some(ruid) = ruid {
|
||||
unistd::setuid(ruid).map_err(|err| err.into_pyexception(vm))?;
|
||||
}
|
||||
if let Some(euid) = euid {
|
||||
unistd::seteuid(euid).map_err(|err| err.into_pyexception(vm))?;
|
||||
}
|
||||
Ok(())
|
||||
fn setreuid(ruid: Uid, euid: Uid) -> nix::Result<()> {
|
||||
let ret = unsafe { libc::setreuid(ruid.as_raw(), euid.as_raw()) };
|
||||
nix::Error::result(ret).map(drop)
|
||||
}
|
||||
|
||||
// cfg from nix
|
||||
@@ -1209,20 +1211,8 @@ pub mod module {
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
#[pyfunction]
|
||||
fn setresuid(
|
||||
ruid: Option<Uid>,
|
||||
euid: Option<Uid>,
|
||||
suid: Option<Uid>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let unwrap_or_unchanged =
|
||||
|u: Option<Uid>| u.unwrap_or_else(|| Uid::from_raw(libc::uid_t::MAX));
|
||||
unistd::setresuid(
|
||||
unwrap_or_unchanged(ruid),
|
||||
unwrap_or_unchanged(euid),
|
||||
unwrap_or_unchanged(suid),
|
||||
)
|
||||
.map_err(|err| err.into_pyexception(vm))
|
||||
fn setresuid(ruid: Uid, euid: Uid, suid: Uid) -> nix::Result<()> {
|
||||
unistd::setresuid(ruid, euid, suid)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
@@ -1271,8 +1261,8 @@ pub mod module {
|
||||
// cfg from nix
|
||||
#[cfg(any(target_os = "android", target_os = "linux", target_os = "openbsd"))]
|
||||
#[pyfunction]
|
||||
fn getresuid(vm: &VirtualMachine) -> PyResult<(u32, u32, u32)> {
|
||||
let ret = unistd::getresuid().map_err(|e| e.into_pyexception(vm))?;
|
||||
fn getresuid() -> nix::Result<(u32, u32, u32)> {
|
||||
let ret = unistd::getresuid()?;
|
||||
Ok((
|
||||
ret.real.as_raw(),
|
||||
ret.effective.as_raw(),
|
||||
@@ -1283,8 +1273,8 @@ pub mod module {
|
||||
// cfg from nix
|
||||
#[cfg(any(target_os = "android", target_os = "linux", target_os = "openbsd"))]
|
||||
#[pyfunction]
|
||||
fn getresgid(vm: &VirtualMachine) -> PyResult<(u32, u32, u32)> {
|
||||
let ret = unistd::getresgid().map_err(|e| e.into_pyexception(vm))?;
|
||||
fn getresgid() -> nix::Result<(u32, u32, u32)> {
|
||||
let ret = unistd::getresgid()?;
|
||||
Ok((
|
||||
ret.real.as_raw(),
|
||||
ret.effective.as_raw(),
|
||||
@@ -1300,32 +1290,15 @@ pub mod module {
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
#[pyfunction]
|
||||
fn setresgid(
|
||||
rgid: Option<Gid>,
|
||||
egid: Option<Gid>,
|
||||
sgid: Option<Gid>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let unwrap_or_unchanged =
|
||||
|u: Option<Gid>| u.unwrap_or_else(|| Gid::from_raw(libc::gid_t::MAX));
|
||||
unistd::setresgid(
|
||||
unwrap_or_unchanged(rgid),
|
||||
unwrap_or_unchanged(egid),
|
||||
unwrap_or_unchanged(sgid),
|
||||
)
|
||||
.map_err(|err| err.into_pyexception(vm))
|
||||
fn setresgid(rgid: Gid, egid: Gid, sgid: Gid, vm: &VirtualMachine) -> PyResult<()> {
|
||||
unistd::setresgid(rgid, egid, sgid).map_err(|err| err.into_pyexception(vm))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
#[pyfunction]
|
||||
fn setregid(rgid: Option<Gid>, egid: Option<Gid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
if let Some(rgid) = rgid {
|
||||
unistd::setgid(rgid).map_err(|err| err.into_pyexception(vm))?;
|
||||
}
|
||||
if let Some(egid) = egid {
|
||||
unistd::setegid(egid).map_err(|err| err.into_pyexception(vm))?;
|
||||
}
|
||||
Ok(())
|
||||
fn setregid(rgid: Gid, egid: Gid) -> nix::Result<()> {
|
||||
let ret = unsafe { libc::setregid(rgid.as_raw(), egid.as_raw()) };
|
||||
nix::Error::result(ret).map(drop)
|
||||
}
|
||||
|
||||
// cfg from nix
|
||||
@@ -1336,10 +1309,8 @@ pub mod module {
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
#[pyfunction]
|
||||
fn initgroups(user_name: PyStrRef, gid: Option<Gid>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
fn initgroups(user_name: PyStrRef, gid: Gid, vm: &VirtualMachine) -> PyResult<()> {
|
||||
let user = user_name.to_cstring(vm)?;
|
||||
let gid =
|
||||
gid.ok_or_else(|| vm.new_errno_error(1, "Operation not permitted".to_string()))?;
|
||||
unistd::initgroups(&user, gid).map_err(|err| err.into_pyexception(vm))
|
||||
}
|
||||
|
||||
@@ -1347,15 +1318,11 @@ pub mod module {
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "redox")))]
|
||||
#[pyfunction]
|
||||
fn setgroups(
|
||||
group_ids: crate::function::ArgIterable<Option<Gid>>,
|
||||
group_ids: crate::function::ArgIterable<Gid>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let gids = group_ids
|
||||
.iter(vm)?
|
||||
.collect::<Result<Option<Vec<_>>, _>>()?
|
||||
.ok_or_else(|| vm.new_errno_error(1, "Operation not permitted".to_string()))?;
|
||||
let ret = unistd::setgroups(&gids);
|
||||
ret.map_err(|err| err.into_pyexception(vm))
|
||||
let gids = group_ids.iter(vm)?.collect::<Result<Vec<_>, _>>()?;
|
||||
unistd::setgroups(&gids).map_err(|err| err.into_pyexception(vm))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
|
||||
|
||||
@@ -97,12 +97,19 @@ mod decl {
|
||||
_time(vm)
|
||||
}
|
||||
|
||||
#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
not(any(target_os = "emscripten", target_os = "wasi")),
|
||||
)))]
|
||||
fn _time(vm: &VirtualMachine) -> PyResult<f64> {
|
||||
Ok(duration_since_system_now(vm)?.as_secs_f64())
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "wasmbind",
|
||||
not(any(target_os = "emscripten", target_os = "wasi"))
|
||||
))]
|
||||
fn _time(_vm: &VirtualMachine) -> PyResult<f64> {
|
||||
use wasm_bindgen::prelude::*;
|
||||
#[wasm_bindgen]
|
||||
@@ -115,6 +122,15 @@ mod decl {
|
||||
Ok(Date::now() / 1000.0)
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
not(feature = "wasmbind"),
|
||||
not(any(target_os = "emscripten", target_os = "wasi"))
|
||||
))]
|
||||
fn _time(vm: &VirtualMachine) -> PyResult<f64> {
|
||||
Err(vm.new_not_implemented_error("time.time".to_owned()))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn monotonic(vm: &VirtualMachine) -> PyResult<f64> {
|
||||
Ok(get_monotonic_time(vm)?.as_secs_f64())
|
||||
|
||||
@@ -13,13 +13,21 @@ impl VirtualMachine {
|
||||
#[track_caller]
|
||||
#[cold]
|
||||
fn _py_panic_failed(&self, exc: PyBaseExceptionRef, msg: &str) -> ! {
|
||||
#[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))]
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
not(any(target_os = "emscripten", target_os = "wasi")),
|
||||
)))]
|
||||
{
|
||||
self.print_exception(exc);
|
||||
self.flush_std();
|
||||
panic!("{msg}")
|
||||
}
|
||||
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "wasmbind",
|
||||
not(any(target_os = "emscripten", target_os = "wasi")),
|
||||
))]
|
||||
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))]
|
||||
{
|
||||
use wasm_bindgen::prelude::*;
|
||||
#[wasm_bindgen]
|
||||
@@ -32,6 +40,17 @@ impl VirtualMachine {
|
||||
error(&s);
|
||||
panic!("{}; exception backtrace above", msg)
|
||||
}
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
not(feature = "wasmbind"),
|
||||
not(any(target_os = "emscripten", target_os = "wasi")),
|
||||
))]
|
||||
{
|
||||
use crate::convert::ToPyObject;
|
||||
let err_string: String = exc.to_pyobject(self).repr(self).unwrap().to_string();
|
||||
eprintln!("{err_string}");
|
||||
panic!("{}; python exception not available", msg)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn flush_std(&self) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
[package]
|
||||
name = "rustpython-sre_engine"
|
||||
version = "0.4.0"
|
||||
authors = ["Kangzhi Shi <shikangzhi@gmail.com>", "RustPython Team"]
|
||||
description = "A low-level implementation of Python's SRE regex engine"
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
keywords = ["regex"]
|
||||
include = ["LICENSE", "src/**/*.rs"]
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
num_enum = { workspace = true }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"raw-loader": "1.0.0",
|
||||
"webpack": "4.28.2",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-cli": "^3.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "rustpython_wasm"
|
||||
version = "0.4.0"
|
||||
authors = ["RustPython Team"]
|
||||
license = "MIT"
|
||||
description = "A Python-3 (CPython >= 3.5.0) Interpreter written in Rust, compiled to WASM"
|
||||
repository = "https://github.com/RustPython/RustPython/tree/main/wasm/lib"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -20,16 +21,16 @@ rustpython-common = { workspace = true }
|
||||
rustpython-pylib = { workspace = true, optional = true }
|
||||
rustpython-stdlib = { workspace = true, default-features = false, optional = true }
|
||||
# make sure no threading! otherwise wasm build will fail
|
||||
rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde"] }
|
||||
rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde", "wasmbind"] }
|
||||
|
||||
rustpython-parser = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
|
||||
console_error_panic_hook = "0.1"
|
||||
js-sys = "0.3"
|
||||
serde-wasm-bindgen = "0.3.1"
|
||||
wasm-bindgen = "0.2.80"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
|
||||
13
wasm/wasm-unknown-test/Cargo.toml
Normal file
13
wasm/wasm-unknown-test/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "wasm-unknown-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
getrandom = { version = "0.2.12", features = ["custom"] }
|
||||
rustpython-vm = { path = "../../vm", default-features = false, features = ["compiler"] }
|
||||
|
||||
[workspace]
|
||||
1
wasm/wasm-unknown-test/README.md
Normal file
1
wasm/wasm-unknown-test/README.md
Normal file
@@ -0,0 +1 @@
|
||||
A test crate to ensure that `rustpython-vm` compiles on `wasm32-unknown-unknown` without a JS host.
|
||||
16
wasm/wasm-unknown-test/src/lib.rs
Normal file
16
wasm/wasm-unknown-test/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use rustpython_vm::{eval, Interpreter};
|
||||
|
||||
pub unsafe extern "C" fn eval(s: *const u8, l: usize) -> u32 {
|
||||
let src = std::slice::from_raw_parts(s, l);
|
||||
let src = std::str::from_utf8(src).unwrap();
|
||||
Interpreter::without_stdlib(Default::default()).enter(|vm| {
|
||||
let res = eval::eval(vm, src, vm.new_scope_with_builtins(), "<string>").unwrap();
|
||||
res.try_into_value(vm).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
fn getrandom_always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
|
||||
Err(getrandom::Error::UNSUPPORTED)
|
||||
}
|
||||
|
||||
getrandom::register_custom_getrandom!(getrandom_always_fail);
|
||||
Reference in New Issue
Block a user