Compare commits

..

33 Commits

Author SHA1 Message Date
Noa
e534b10722 Fix build with ossl1 2024-09-22 17:38:26 +09:00
Jeong, YunWon
0785cc5aa9 Merge pull request #5406 from coolreader18/improve-posixsubprocess
Improve posixsubprocess
2024-09-22 17:37:34 +09:00
Noa
eeb719e8f7 Merge pull request #5411 from crazymerlyn/decimal-from-float
Fix inconsistent behavior of Decimal.from_float
2024-09-21 22:58:23 -05:00
Noa
7933edad43 Add missing functionality to posixsubprocess 2024-09-21 22:40:15 -05:00
Ankit Goel
5f75728ede Fix inconsistent behavior of Decimal.from_float 2024-09-22 01:42:28 +01:00
Ankit Goel
8cff0ed6c2 Avoid allocating a vector of elements when hashing frozenset (#5408)
Adds a `try_fold_keys` method to Dict which allows performing common
operations on all elements without needing to create a Vec first.
2024-09-21 23:18:26 +09:00
Noa
a8964f4108 Add select.epoll 2024-09-20 11:46:01 +09:00
Noa
740aeedca3 Merge pull request #5405 from crazymerlyn/fix-set-intersection-update
Fix set intersection_update implementation
2024-09-19 21:21:16 -05:00
Noa
8152e7e62c Make Gid/Uid less janky 2024-09-19 17:50:52 -05:00
Ankit Goel
b36c95b91e Fix set intersection_update implementation 2024-09-19 20:59:12 +01:00
Ricardo Robles
b5c1fd95dc Add zoneinfo from Python 3.12.6 (#5400) 2024-09-19 11:18:48 +09:00
Noa
23f7cbf1c3 Make sqlite optional 2024-09-19 11:13:51 +09:00
hongmengning
ae78ecc2c5 Add missing symbols in exceptions.rs 2024-09-17 16:07:30 +09:00
Ankit Goel
dd06516d1c Deprecate delegating int() to __trunc__ 2024-09-17 16:06:09 +09:00
Ankit Goel
8066f36a4e Fix copysign behavior for NaNs (#5396) 2024-09-11 15:19:21 +09:00
Jeong YunWon
3bbf6b9d53 less strict time bound for test_re.test_search_anchor_at_beginning 2024-09-10 16:31:43 +09:00
dependabot[bot]
8bc5944178 Bump webpack from 4.28.2 to 5.94.0 in /wasm/example
Bumps [webpack](https://github.com/webpack/webpack) from 4.28.2 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.28.2...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 14:14:51 +09:00
Niels Buwen
a13b99642b Add get/set methods for function module/annotations (#5392)
---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2024-08-19 00:59:51 +09:00
Jeong, YunWon
060db5983c Merge pull request #5390 from youknowone/wasmbind
recreating wasmbind patch
2024-08-09 09:37:49 +09:00
Jeong YunWon
42bba6920e apply suggestion 2024-08-09 02:23:49 +09:00
Noa
ea11d78995 Fix compilation without compiler feature 2024-08-09 02:23:49 +09:00
Jeong YunWon
fe63ca762f separate wasm-unknown-test 2024-08-09 02:23:49 +09:00
Jeong YunWon
a82982725e fix getrandom 2024-08-09 02:23:49 +09:00
Noa
7dfb760421 Make rustpython-vm compatible with non-js wasm32-unknown & add tests 2024-08-09 02:04:25 +09:00
Jeong YunWon
b6e9a3f37e rustpython_wasm uses the new feature 2024-08-09 00:59:29 +09:00
Jeong YunWon
3f28309b7b revert unnecessary change 2024-08-09 00:58:53 +09:00
Jeong YunWon
d2a4a330f9 following chrono/wasmbind convention 2024-08-09 00:58:11 +09:00
Benjamin DeMann
d8c35770ab enable js feature of getrandom only for wasmbind 2024-08-09 00:29:47 +09:00
Benjamin DeMann
dbb6794a41 add cfg for not wasmbind for time 2024-08-09 00:29:25 +09:00
Benjamin DeMann
63c9909aa0 run the first block if wasmbind is not present 2024-08-09 00:29:25 +09:00
Benjamin DeMann
f1dac5087e address pr notes 2024-08-09 00:29:25 +09:00
Benjamin DeMann
4f80d7013e add wasmbind feature 2024-08-09 00:29:25 +09:00
Jeong YunWon
2919df1df5 Mark rust-version 1.78 2024-08-08 23:20:17 +09:00
63 changed files with 5376 additions and 468 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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
View File

@@ -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:

View 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

View 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)]

View 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
View 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")

View File

@@ -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.)

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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
View File

@@ -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

View File

@@ -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()

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
import unittest
unittest.main('test.test_zoneinfo')

100
Lib/test/test_zoneinfo/_support.py vendored Normal file
View 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)

View File

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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
View 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

View File

@@ -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"]

View File

@@ -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 }

View File

@@ -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"] }

View File

@@ -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"] }

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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") {

View File

@@ -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)?
};

View File

@@ -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,
}

View File

@@ -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:

View File

@@ -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(())
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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)
}

View File

@@ -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 }

View File

@@ -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()

View File

@@ -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);

View File

@@ -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>(

View File

@@ -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),

View File

@@ -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)

View File

@@ -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)

View File

@@ -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!(

View File

@@ -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::*;

View File

@@ -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 {

View File

@@ -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"))]

View File

@@ -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())

View File

@@ -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) {

View File

@@ -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 }

View File

@@ -6,7 +6,7 @@
},
"devDependencies": {
"raw-loader": "1.0.0",
"webpack": "4.28.2",
"webpack": "5.94.0",
"webpack-cli": "^3.1.2"
},
"scripts": {

View File

@@ -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",

View 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]

View File

@@ -0,0 +1 @@
A test crate to ensure that `rustpython-vm` compiles on `wasm32-unknown-unknown` without a JS host.

View 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);