Skip to content

Commit

Permalink
[numpy] numpy-legacy-random
Browse files Browse the repository at this point in the history
  • Loading branch information
sbrugman committed Feb 16, 2023
1 parent 028c785 commit 8e611f6
Show file tree
Hide file tree
Showing 10 changed files with 755 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,7 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| NPY001 | [numpy-deprecated-type-alias](https://beta.ruff.rs/docs/rules/numpy-deprecated-type-alias/) | Type alias `np.{type_name}` is deprecated, replace with builtin type | 🛠 |
| NPY002 | [numpy-legacy-random](https://beta.ruff.rs/docs/rules/numpy-legacy-random/) | Using legacy method `np.random.{method_name}`, replace with the new random number generator | |

### Ruff-specific rules (RUF)

Expand Down
62 changes: 62 additions & 0 deletions crates/ruff/resources/test/fixtures/numpy/NPY002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Do this (new version)
from numpy.random import default_rng
rng = default_rng()
vals = rng.standard_normal(10)
more_vals = rng.standard_normal(10)
numbers = rng.integers(high, size=5)

# instead of this (legacy version)
from numpy import random
vals = random.standard_normal(10)
more_vals = random.standard_normal(10)
numbers = random.integers(high, size=5)

import numpy
numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()
numpy.random.rand()
numpy.random.randn()
numpy.random.randint()
numpy.random.random_integers()
numpy.random.random_sample()
numpy.random.choice()
numpy.random.bytes()
numpy.random.shuffle()
numpy.random.permutation()
numpy.random.beta()
numpy.random.binomial()
numpy.random.chisquare()
numpy.random.dirichlet()
numpy.random.exponential()
numpy.random.f()
numpy.random.gamma()
numpy.random.geometric()
numpy.random.get_state()
numpy.random.gumbel()
numpy.random.hypergeometric()
numpy.random.laplace()
numpy.random.logistic()
numpy.random.lognormal()
numpy.random.logseries()
numpy.random.multinomial()
numpy.random.multivariate_normal()
numpy.random.negative_binomial()
numpy.random.noncentral_chisquare()
numpy.random.noncentral_f()
numpy.random.normal()
numpy.random.pareto()
numpy.random.poisson()
numpy.random.power()
numpy.random.rayleigh()
numpy.random.standard_cauchy()
numpy.random.standard_exponential()
numpy.random.standard_gamma()
numpy.random.standard_normal()
numpy.random.standard_t()
numpy.random.triangular()
numpy.random.uniform()
numpy.random.vonmises()
numpy.random.wald()
numpy.random.weibull()
numpy.random.zipf()
5 changes: 5 additions & 0 deletions crates/ruff/src/checkers/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,11 @@ where
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}

// numpy
if self.settings.rules.enabled(&Rule::NumpyLegacyRandom) {
numpy::rules::numpy_legacy_random(self, func);
}

// flake8-logging-format
if self.settings.rules.enabled(&Rule::LoggingStringFormat)
|| self.settings.rules.enabled(&Rule::LoggingPercentFormat)
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {

// numpy
(Numpy, "001") => Rule::NumpyDeprecatedTypeAlias,
(Numpy, "002") => Rule::NumpyLegacyRandom,

// ruff
(Ruff, "001") => Rule::AmbiguousUnicodeCharacterString,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ ruff_macros::register_rules!(
rules::flake8_self::rules::PrivateMemberAccess,
// numpy
rules::numpy::rules::NumpyDeprecatedTypeAlias,
rules::numpy::rules::NumpyLegacyRandom,
// ruff
rules::ruff::rules::AmbiguousUnicodeCharacterString,
rules::ruff::rules::AmbiguousUnicodeCharacterDocstring,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/rules/numpy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod tests {
use crate::{assert_yaml_snapshot, settings};

#[test_case(Rule::NumpyDeprecatedTypeAlias, Path::new("NPY001.py"); "NPY001")]
#[test_case(Rule::NumpyLegacyRandom, Path::new("NPY002.py"); "NPY002")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/numpy/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub use deprecated_type_alias::{deprecated_type_alias, NumpyDeprecatedTypeAlias};
pub use numpy_legacy_random::{numpy_legacy_random, NumpyLegacyRandom};

mod deprecated_type_alias;
mod numpy_legacy_random;
126 changes: 126 additions & 0 deletions crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::Expr;

use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;

define_violation!(
/// ## What it does
/// Checks for legacy NumPy random functions.
///
/// ## Why is this bad?
/// According to the NumPy documentation the [Legacy Random Generation]:
/// > The `RandomState` provides access to legacy generators. This generator is considered frozen and will
/// > have no further improvements. It is guaranteed to produce the same values as the final point release
/// > of NumPy v1.16. These all depend on Box-Muller normals or inverse CDF exponentials or gammas. This class
/// > should only be used if it is essential to have randoms that are identical to what would have been produced
/// > by previous versions of NumPy.
///
/// This is a convenience, legacy function that exists to support older code that uses the singleton `RandomState`.
/// Best practice is to use a dedicated `Generator` instance rather than the random variate generation methods
/// exposed directly in the random module.
///
/// The new `Generator` uses bits provided by `PCG64` which by default has better statistical properties than the
/// legacy `MT19937` used in `RandomState`. See the documentation on [Random Sampling] and [NEP 19] for further details.
///
///
/// ## Examples
/// ```python
/// import numpy as np
///
/// np.random.seed(1337)
/// np.random.normal()
/// ```
///
/// Use instead:
/// ```python
/// rng = np.random.default_rng(1337)
/// rng.normal()
/// ```
///
/// [Legacy Random Generation]: https://numpy.org/doc/stable/reference/random/legacy.html#legacy
/// [Random Sampling]: https://numpy.org/doc/stable/reference/random/index.html#random-quick-start
/// [NEP 19]: https://numpy.org/neps/nep-0019-rng-policy.html
pub struct NumpyLegacyRandom {
pub method_name: String,
}
);
impl Violation for NumpyLegacyRandom {
#[derive_message_formats]
fn message(&self) -> String {
let NumpyLegacyRandom { method_name } = self;
format!("Using legacy method `np.random.{method_name}`, replace with the new random number generator")
}
}

/// NPY002
pub fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) {
if let Some(method_name) = checker.resolve_call_path(expr).and_then(|call_path| {
// seeding state
if call_path.as_slice() == ["numpy", "random", "seed"]
|| call_path.as_slice() == ["numpy", "random", "get_state"]
|| call_path.as_slice() == ["numpy", "random", "set_state"]
// simple random data
|| call_path.as_slice() == ["numpy", "random", "rand"]
|| call_path.as_slice() == ["numpy", "random", "randn"]
|| call_path.as_slice() == ["numpy", "random", "randint"]
|| call_path.as_slice() == ["numpy", "random", "random_integers"]
|| call_path.as_slice() == ["numpy", "random", "random_sample"]
|| call_path.as_slice() == ["numpy", "random", "choice"]
|| call_path.as_slice() == ["numpy", "random", "bytes"]
// permutations
|| call_path.as_slice() == ["numpy", "random", "shuffle"]
|| call_path.as_slice() == ["numpy", "random", "permutation"]
// distributions
|| call_path.as_slice() == ["numpy", "random", "beta"]
|| call_path.as_slice() == ["numpy", "random", "binomial"]
|| call_path.as_slice() == ["numpy", "random", "chisquare"]
|| call_path.as_slice() == ["numpy", "random", "dirichlet"]
|| call_path.as_slice() == ["numpy", "random", "exponential"]
|| call_path.as_slice() == ["numpy", "random", "f"]
|| call_path.as_slice() == ["numpy", "random", "gamma"]
|| call_path.as_slice() == ["numpy", "random", "geometric"]
|| call_path.as_slice() == ["numpy", "random", "get_state"]
|| call_path.as_slice() == ["numpy", "random", "gumbel"]
|| call_path.as_slice() == ["numpy", "random", "hypergeometric"]
|| call_path.as_slice() == ["numpy", "random", "laplace"]
|| call_path.as_slice() == ["numpy", "random", "logistic"]
|| call_path.as_slice() == ["numpy", "random", "lognormal"]
|| call_path.as_slice() == ["numpy", "random", "logseries"]
|| call_path.as_slice() == ["numpy", "random", "multinomial"]
|| call_path.as_slice() == ["numpy", "random", "multivariate_normal"]
|| call_path.as_slice() == ["numpy", "random", "negative_binomial"]
|| call_path.as_slice() == ["numpy", "random", "noncentral_chisquare"]
|| call_path.as_slice() == ["numpy", "random", "noncentral_f"]
|| call_path.as_slice() == ["numpy", "random", "normal"]
|| call_path.as_slice() == ["numpy", "random", "pareto"]
|| call_path.as_slice() == ["numpy", "random", "poisson"]
|| call_path.as_slice() == ["numpy", "random", "power"]
|| call_path.as_slice() == ["numpy", "random", "rayleigh"]
|| call_path.as_slice() == ["numpy", "random", "standard_cauchy"]
|| call_path.as_slice() == ["numpy", "random", "standard_exponential"]
|| call_path.as_slice() == ["numpy", "random", "standard_gamma"]
|| call_path.as_slice() == ["numpy", "random", "standard_normal"]
|| call_path.as_slice() == ["numpy", "random", "standard_t"]
|| call_path.as_slice() == ["numpy", "random", "triangular"]
|| call_path.as_slice() == ["numpy", "random", "uniform"]
|| call_path.as_slice() == ["numpy", "random", "vonmises"]
|| call_path.as_slice() == ["numpy", "random", "wald"]
|| call_path.as_slice() == ["numpy", "random", "weibull"]
|| call_path.as_slice() == ["numpy", "random", "zipf"]
{
Some(call_path[2])
} else {
None
}
}) {
checker.diagnostics.push(Diagnostic::new(
NumpyLegacyRandom {
method_name: method_name.to_string(),
},
Range::from_located(expr),
));
}
}
Loading

0 comments on commit 8e611f6

Please sign in to comment.