diff --git a/Cargo.lock b/Cargo.lock index 4f9f4ec..f24088c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,13 +187,13 @@ dependencies = [ "rkyv", "serde", "tracing", - "unicode-width", + "unicode-width 0.1.13", "unscanny", ] [[package]] name = "pep508_rs" -version = "0.6.1" +version = "0.7.0" dependencies = [ "indoc", "log", @@ -208,7 +208,7 @@ dependencies = [ "testing_logger", "thiserror", "tracing", - "unicode-width", + "unicode-width 0.2.0", "url", "urlencoding", ] @@ -603,6 +603,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unindent" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 781a275..63a1acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pep508_rs" -version = "0.6.1" +version = "0.7.0" description = "A library for python dependency specifiers, better known as PEP 508" edition = "2021" include = ["/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml"] @@ -24,7 +24,7 @@ serde = { version = "1.0.198", features = ["derive"], optional = true } serde_json = { version = "1.0.116", optional = true } thiserror = "1.0.59" tracing = { version = "0.1.40", optional = true } -unicode-width = "0.1.11" +unicode-width = "0.2.0" url = "2.5.0" urlencoding = "2.1.3" diff --git a/Changelog.md b/Changelog.md index 4ebe735..56c85ad 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,27 +1,31 @@ +# 0.7.0 + +- Remove pyo3 + # 0.6.1 -* Update to pyo3 0.22 +- Update to pyo3 0.22 # 0.6.0 -* Added `origin` to `Requirement` +- Added `origin` to `Requirement` # 0.5.0 -* Update to pyo3 0.21 -* Update to pyo3-log 0.1.0 +- Update to pyo3 0.21 +- Update to pyo3-log 0.1.0 # v0.4.2 -* CI fixes, mac os builds are temporarily disabled. +- CI fixes, mac os builds are temporarily disabled. # v0.4.1 -* CI fixes, mac os builds are temporarily disabled. +- CI fixes, mac os builds are temporarily disabled. # v0.4.0 -* Package and extra names are now validated and normalized. -* Updated `pep440_rs` to 0.5.0. -* [rkyv](https://github.com/rkyv/rkyv) support. -* `tracing` is now a separate feature. +- Package and extra names are now validated and normalized. +- Updated `pep440_rs` to 0.5.0. +- [rkyv](https://github.com/rkyv/rkyv) support. +- `tracing` is now a separate feature. diff --git a/Readme.md b/Readme.md index c894894..88581e6 100644 --- a/Readme.md +++ b/Readme.md @@ -56,13 +56,12 @@ assert Requirement( ).evaluate_markers(env, ["science"]) ``` - ```python from pep508_rs import Requirement, MarkerEnvironment env = MarkerEnvironment.current() Requirement("numpy; python_version >= '3.9.'").evaluate_markers(env, []) -# This will log: +# This will log: # "Expected PEP 440 version to compare with python_version, found '3.9.', " # "evaluating to false: Version `3.9.` doesn't match PEP 440 rules" ``` diff --git a/src/lib.rs b/src/lib.rs index 9fcf802..f6c9d60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,23 +16,12 @@ #![warn(missing_docs)] use origin::RequirementOrigin; -#[cfg(feature = "pyo3")] -use pep440_rs::PyVersion; use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; -#[cfg(feature = "pyo3")] -use pyo3::{ - create_exception, exceptions::PyNotImplementedError, pyclass, pyclass::CompareOp, pymethods, - pymodule, types::PyModule, Bound, IntoPy, PyObject, PyResult, Python, -}; #[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; -#[cfg(feature = "pyo3")] -use std::collections::hash_map::DefaultHasher; use std::collections::HashSet; use std::fmt::{Display, Formatter}; -#[cfg(feature = "pyo3")] -use std::hash::{Hash, Hasher}; use std::path::Path; use std::str::{Chars, FromStr}; use thiserror::Error; @@ -113,17 +102,8 @@ impl Display for Pep508Error { /// We need this to allow e.g. anyhow's `.context()` impl std::error::Error for Pep508Error {} -#[cfg(feature = "pyo3")] -create_exception!( - pep508, - PyPep508Error, - pyo3::exceptions::PyValueError, - "A PEP 508 parser error with span information" -); - /// A PEP 508 dependency specification #[derive(Hash, Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "pyo3", pyclass(module = "pep508"))] pub struct Requirement { /// The distribution name such as `numpy` in /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` @@ -213,137 +193,6 @@ impl Serialize for Requirement { type MarkerWarning = (MarkerWarningKind, String, String); -#[cfg(feature = "pyo3")] -#[pymethods] -impl Requirement { - /// The distribution name such as `numpy` in - /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` - #[getter] - pub fn name(&self) -> String { - self.name.to_string() - } - - /// The list of extras such as `security`, `tests` in - /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` - #[getter] - pub fn extras(&self) -> Vec { - self.extras.iter().map(ToString::to_string).collect() - } - - /// The marker expression such as `python_version > "3.8"` in - /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"` - #[getter] - pub fn marker(&self) -> Option { - self.marker.as_ref().map(std::string::ToString::to_string) - } - - /// Parses a PEP 440 string - #[new] - pub fn py_new(requirement: &str) -> PyResult { - Self::from_str(requirement).map_err(|err| PyPep508Error::new_err(err.to_string())) - } - - #[getter] - fn version_or_url(&self, py: Python<'_>) -> PyObject { - match &self.version_or_url { - None => py.None(), - Some(VersionOrUrl::VersionSpecifier(version_specifier)) => version_specifier - .iter() - .map(|x| x.clone().into_py(py)) - .collect::>() - .into_py(py), - Some(VersionOrUrl::Url(url)) => url.to_string().into_py(py), - } - } - - fn __str__(&self) -> String { - self.to_string() - } - - fn __repr__(&self) -> String { - format!(r#""{self}""#) - } - - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - let err = PyNotImplementedError::new_err("Requirement only supports equality comparisons"); - match op { - CompareOp::Lt => Err(err), - CompareOp::Le => Err(err), - CompareOp::Eq => Ok(self == other), - CompareOp::Ne => Ok(self != other), - CompareOp::Gt => Err(err), - CompareOp::Ge => Err(err), - } - } - - fn __hash__(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.hash(&mut hasher); - hasher.finish() - } - - /// Returns whether the markers apply for the given environment - #[allow(clippy::needless_pass_by_value)] - #[pyo3(name = "evaluate_markers")] - pub fn py_evaluate_markers( - &self, - env: &MarkerEnvironment, - extras: Vec, - ) -> PyResult { - let extras = extras - .into_iter() - .map(|extra| ExtraName::from_str(&extra)) - .collect::, InvalidNameError>>() - .map_err(|err| PyPep508Error::new_err(err.to_string()))?; - - Ok(self.evaluate_markers(env, &extras)) - } - - /// Returns whether the requirement would be satisfied, independent of environment markers, i.e. - /// if there is potentially an environment that could activate this requirement. - /// - /// Note that unlike [Self::evaluate_markers] this does not perform any checks for bogus - /// expressions but will simply return true. As caller you should separately perform a check - /// with an environment and forward all warnings. - #[allow(clippy::needless_pass_by_value)] - #[pyo3(name = "evaluate_extras_and_python_version")] - pub fn py_evaluate_extras_and_python_version( - &self, - extras: HashSet, - python_versions: Vec, - ) -> PyResult { - let extras = extras - .into_iter() - .map(|extra| ExtraName::from_str(&extra)) - .collect::, InvalidNameError>>() - .map_err(|err| PyPep508Error::new_err(err.to_string()))?; - - let python_versions = python_versions - .into_iter() - .map(|py_version| py_version.0) - .collect::>(); - - Ok(self.evaluate_extras_and_python_version(&extras, &python_versions)) - } - - /// Returns whether the markers apply for the given environment - #[allow(clippy::needless_pass_by_value)] - #[pyo3(name = "evaluate_markers_and_report")] - pub fn py_evaluate_markers_and_report( - &self, - env: &MarkerEnvironment, - extras: Vec, - ) -> PyResult<(bool, Vec)> { - let extras = extras - .into_iter() - .map(|extra| ExtraName::from_str(&extra)) - .collect::, InvalidNameError>>() - .map_err(|err| PyPep508Error::new_err(err.to_string()))?; - - Ok(self.evaluate_markers_and_report(env, &extras)) - } -} - impl Requirement { /// Returns `true` if the [`Version`] satisfies the [`Requirement`]. pub fn is_satisfied_by(&self, version: &Version) -> bool { @@ -1014,32 +863,6 @@ fn parse(cursor: &mut Cursor, working_dir: Option<&Path>) -> Result, m: &Bound) -> PyResult<()> { - use pyo3::prelude::*; - // Allowed to fail if we embed this module in another - #[allow(unused_must_use)] - { - pyo3_log::try_init(); - } - - m.add_class::()?; - m.add_class::()?; - - m.add_class::()?; - m.add_class::()?; - m.add("Pep508Error", py.get_type_bound::())?; - Ok(()) -} - /// Half of these tests are copied from #[cfg(test)] mod tests { diff --git a/src/marker.rs b/src/marker.rs index b67339d..6d1c12d 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -11,8 +11,6 @@ use crate::{Cursor, ExtraName, Pep508Error, Pep508ErrorSource}; use pep440_rs::{Version, VersionPattern, VersionSpecifier}; -#[cfg(feature = "pyo3")] -use pyo3::{exceptions::PyValueError, prelude::PyAnyMethods, pyclass, pymethods, PyResult, Python}; #[cfg(feature = "serde")] use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashSet; @@ -22,10 +20,6 @@ use std::str::FromStr; /// Ways in which marker evaluation can fail #[derive(Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Clone, Copy)] -#[cfg_attr( - feature = "pyo3", - pyclass(module = "pep508", frozen, hash, ord, eq, eq_int) -)] pub enum MarkerWarningKind { /// Using an old name from PEP 345 instead of the modern equivalent /// @@ -279,7 +273,6 @@ impl Display for MarkerOperator { /// Helper type with a [Version] and its original text #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "pyo3", pyclass(get_all, module = "pep508"))] pub struct StringVersion { /// Original unchanged string pub string: String, @@ -340,7 +333,6 @@ impl Deref for StringVersion { /// Some are `(String, Version)` because we have to support version comparison #[allow(missing_docs, clippy::unsafe_derive_deserialize)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "pyo3", pyclass(get_all, module = "pep508"))] #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct MarkerEnvironment { pub implementation_name: String, @@ -390,135 +382,6 @@ impl MarkerEnvironment { } } -#[cfg(feature = "pyo3")] -#[pymethods] -impl MarkerEnvironment { - /// Construct your own marker environment - #[new] - #[pyo3(signature = (*, - implementation_name, - implementation_version, - os_name, - platform_machine, - platform_python_implementation, - platform_release, - platform_system, - platform_version, - python_full_version, - python_version, - sys_platform - ))] - #[allow(clippy::too_many_arguments)] - fn py_new( - implementation_name: &str, - implementation_version: &str, - os_name: &str, - platform_machine: &str, - platform_python_implementation: &str, - platform_release: &str, - platform_system: &str, - platform_version: &str, - python_full_version: &str, - python_version: &str, - sys_platform: &str, - ) -> PyResult { - let implementation_version = - StringVersion::from_str(implementation_version).map_err(|err| { - PyValueError::new_err(format!( - "implementation_version is not a valid PEP440 version: {err}" - )) - })?; - let python_full_version = StringVersion::from_str(python_full_version).map_err(|err| { - PyValueError::new_err(format!( - "python_full_version is not a valid PEP440 version: {err}" - )) - })?; - let python_version = StringVersion::from_str(python_version).map_err(|err| { - PyValueError::new_err(format!( - "python_version is not a valid PEP440 version: {err}" - )) - })?; - Ok(Self { - implementation_name: implementation_name.to_string(), - implementation_version, - os_name: os_name.to_string(), - platform_machine: platform_machine.to_string(), - platform_python_implementation: platform_python_implementation.to_string(), - platform_release: platform_release.to_string(), - platform_system: platform_system.to_string(), - platform_version: platform_version.to_string(), - python_full_version, - python_version, - sys_platform: sys_platform.to_string(), - }) - } - - /// Query the current python interpreter to get the correct marker value - #[staticmethod] - fn current(py: Python<'_>) -> PyResult { - let os = py.import_bound("os")?; - let platform = py.import_bound("platform")?; - let sys = py.import_bound("sys")?; - let python_version_tuple: (String, String, String) = platform - .getattr("python_version_tuple")? - .call0()? - .extract()?; - - // See pseudocode at - // https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers - let name = sys.getattr("implementation")?.getattr("name")?.extract()?; - let info = sys.getattr("implementation")?.getattr("version")?; - let kind = info.getattr("releaselevel")?.extract::()?; - let implementation_version: String = format!( - "{}.{}.{}{}", - info.getattr("major")?.extract::()?, - info.getattr("minor")?.extract::()?, - info.getattr("micro")?.extract::()?, - if kind == "final" { - String::new() - } else { - format!("{}{}", kind, info.getattr("serial")?.extract::()?) - } - ); - let python_full_version: String = platform.getattr("python_version")?.call0()?.extract()?; - let python_version = format!("{}.{}", python_version_tuple.0, python_version_tuple.1); - - // This is not written down in PEP 508, but it's the only reasonable assumption to make - let implementation_version = - StringVersion::from_str(&implementation_version).map_err(|err| { - PyValueError::new_err(format!( - "Broken python implementation, implementation_version is not a valid PEP440 version: {err}" - )) - })?; - let python_full_version = StringVersion::from_str(&python_full_version).map_err(|err| { - PyValueError::new_err(format!( - "Broken python implementation, python_full_version is not a valid PEP440 version: {err}" - )) - })?; - let python_version = StringVersion::from_str(&python_version).map_err(|err| { - PyValueError::new_err(format!( - "Broken python implementation, python_version is not a valid PEP440 version: {err}" - )) - })?; - Ok(Self { - implementation_name: name, - implementation_version, - os_name: os.getattr("name")?.extract()?, - platform_machine: platform.getattr("machine")?.call0()?.extract()?, - platform_python_implementation: platform - .getattr("python_implementation")? - .call0()? - .extract()?, - platform_release: platform.getattr("release")?.call0()?.extract()?, - platform_system: platform.getattr("system")?.call0()?.extract()?, - platform_version: platform.getattr("version")?.call0()?.extract()?, - python_full_version, - python_version, - sys_platform: sys.getattr("platform")?.extract()?, - }) - } -} - /// Represents one clause such as `python_version > "3.8"` in the form /// ```text /// diff --git a/test/test_pep508.py b/test/test_pep508.py deleted file mode 100644 index 19d419d..0000000 --- a/test/test_pep508.py +++ /dev/null @@ -1,98 +0,0 @@ -from collections import namedtuple -from unittest import mock - -import pytest -from pep508_rs import Requirement, MarkerEnvironment, Pep508Error, VersionSpecifier - - -def test_pep508(): - req = Requirement("numpy; python_version >= '3.7'") - assert req.name == "numpy" - env = MarkerEnvironment.current() - assert req.evaluate_markers(env, []) - req2 = Requirement("numpy; python_version < '3.7'") - assert not req2.evaluate_markers(env, []) - - requests = Requirement( - 'requests [security,tests] >=2.8.1, ==2.8.* ; python_version > "3.8"' - ) - assert requests.name == "requests" - assert requests.extras == ["security", "tests"] - assert requests.version_or_url == [ - VersionSpecifier(">=2.8.1"), - VersionSpecifier("==2.8.*"), - ] - assert requests.marker == "python_version > '3.8'" - - -def test_marker(): - env = MarkerEnvironment.current() - assert not Requirement("numpy; extra == 'science'").evaluate_markers(env, []) - assert Requirement("numpy; extra == 'science'").evaluate_markers(env, ["science"]) - assert not Requirement( - "numpy; extra == 'science' and extra == 'arrays'" - ).evaluate_markers(env, ["science"]) - assert Requirement( - "numpy; extra == 'science' or extra == 'arrays'" - ).evaluate_markers(env, ["science"]) - - -class FakeVersionInfo( - namedtuple("FakeVersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) -): - pass - - -@pytest.mark.parametrize( - ("version", "version_str"), - [ - (FakeVersionInfo(3, 10, 11, "final", 0), "3.10.11"), - (FakeVersionInfo(3, 10, 11, "rc", 1), "3.10.11rc1"), - ], -) -def test_marker_values(version, version_str): - with mock.patch("sys.implementation.version", version): - env = MarkerEnvironment.current() - assert str(env.implementation_version.version) == version_str - - -def test_marker_values_current_platform(): - MarkerEnvironment.current() - - -def test_errors(): - with pytest.raises( - Pep508Error, - match="Expected an alphanumeric character starting the extra name, found 'ö'", - ): - Requirement("numpy[ö]; python_version < '3.7'") - - -def test_warnings(caplog): - env = MarkerEnvironment.current() - assert not Requirement("numpy; '3.6' < '3.7'").evaluate_markers(env, []) - assert caplog.messages == [ - "Comparing two quoted strings with each other doesn't make sense: " - "'3.6' < '3.7', evaluating to false" - ] - caplog.clear() - assert not Requirement("numpy; 'a' < 'b'").evaluate_markers(env, []) - assert caplog.messages == [ - "Comparing two quoted strings with each other doesn't make sense: " - "'a' < 'b', evaluating to false" - ] - caplog.clear() - Requirement("numpy; python_version >= '3.9.'").evaluate_markers(env, []) - assert caplog.messages == [ - "Expected PEP 440 version to compare with python_version, found '3.9.', " - 'evaluating to false: after parsing 3.9, found "." after it, which is not ' - "part of a valid version", - ] - caplog.clear() - # pickleshare 0.7.5 - Requirement("numpy; python_version in '2.6 2.7 3.2 3.3'").evaluate_markers(env, []) - assert caplog.messages == [ - "Expected PEP 440 version to compare with python_version, found '2.6 2.7 " - '3.2 3.3\', evaluating to false: after parsing 2.6, found "2.7 3.2 3.3" ' - "after it, which is not part of a valid version", - ]