Compare commits

...

15 Commits

Author SHA1 Message Date
Zanie Blue
3ecd263b4d Bump version to 0.0.284 (#6453)
## What's Changed

This release fixes a few bugs, notably the previous release announced a
breaking change where the default target
Python version changed from 3.10 to 3.8 but it was not applied. Thanks
to @rco-ableton for fixing this in
https://github.com/astral-sh/ruff/pull/6444

### Bug Fixes
* Do not trigger `S108` if path is inside `tempfile.*` call by
@dhruvmanila in https://github.com/astral-sh/ruff/pull/6416
* Do not allow on zero tab width by @tjkuson in
https://github.com/astral-sh/ruff/pull/6429
* Fix false-positive in submodule resolution by @charliermarsh in
https://github.com/astral-sh/ruff/pull/6435

## New Contributors
* @rco-ableton made their first contribution in
https://github.com/astral-sh/ruff/pull/6444

**Full Changelog**:
https://github.com/astral-sh/ruff/compare/v0.0.283...v0.0.284
2023-08-09 13:32:33 -05:00
Charlie Marsh
6acf07c5c4 Use latest Python version by default in tests (#6448)
## Summary

Use the same Python version by default for all tests (our
latest-supported version).

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-08-09 15:22:39 +00:00
Charlie Marsh
38b9fb8bbd Set a default on PythonVersion (#6446)
## Summary

I think it makes sense for `PythonVersion::default()` to return our
minimum-supported non-EOL version.

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-08-09 15:19:27 +00:00
dependabot[bot]
e4f57434a2 ci(deps): bump cloudflare/wrangler-action from 2.0.0 to 3.0.0 (#6398)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 2.0.0 to 3.0.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="089567dec4"><code>089567d</code></a>
feat: rewrite Wrangler Action in TypeScript</li>
<li>See full diff in <a
href="https://github.com/cloudflare/wrangler-action/compare/2.0.0...3.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cloudflare/wrangler-action&package-manager=github_actions&previous-version=2.0.0&new-version=3.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:17:43 -05:00
Dhruv Manilawala
6a64f2289b Rename Magic* to IpyEscape* (#6395)
## Summary

This PR renames the `MagicCommand` token to `IpyEscapeCommand` token and
`MagicKind` to `IpyEscapeKind` type to better reflect the purpose of the
token and type. Similarly, it renames the AST nodes from `LineMagic` to
`IpyEscapeCommand` prefixed with `Stmt`/`Expr` wherever necessary.

It also makes renames from using `jupyter_magic` to
`ipython_escape_commands` in various function names.

The mode value is still `Mode::Jupyter` because the escape commands are
part of the IPython syntax but the lexing/parsing is done for a Jupyter
notebook.

### Motivation behind the rename:
* IPython codebase defines it as "EscapeCommand" / "Escape Sequences":
* Escape Sequences:
292e3a2345/IPython/core/inputtransformer2.py (L329-L333)
* Escape command:
292e3a2345/IPython/core/inputtransformer2.py (L410-L411)
* The word "magic" is used mainly for the actual magic commands i.e.,
the ones starting with `%`/`%%`
(https://ipython.readthedocs.io/en/stable/interactive/reference.html#magic-command-system).
So, this avoids any confusion between the Magic token (`%`, `%%`) and
the escape command itself.
## Test Plan

* `cargo test` to make sure all renames are done correctly.
* `grep` for `jupyter_escape`/`magic` to make sure all renames are done
correctly.
2023-08-09 13:28:18 +00:00
Charlie Marsh
3bf1c66cda Group function definition parameters with return type annotations (#6410)
## Summary

This PR removes the group around function definition parameters, instead
grouping the parameters with the type parameters and return type
annotation.

This increases Zulip's similarity score from 0.99385 to 0.99699, so it's
a meaningful improvement. However, there's at least one stability error
that I'm working on, and I'm really just looking for high-level feedback
at this point, because I'm not happy with the solution.

Closes https://github.com/astral-sh/ruff/issues/6352.

## Test Plan

Before:

- `zulip`: 0.99396
- `django`: 0.99784
- `warehouse`: 0.99578
- `build`: 0.75436
- `transformers`: 0.99407
- `cpython`: 0.75987
- `typeshed`: 0.74432

After:

- `zulip`: 0.99702
- `django`: 0.99784
- `warehouse`: 0.99585
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75988
- `typeshed`: 0.74853
2023-08-09 12:13:58 +00:00
rco-ableton
eaada0345c Set default version to py38 (#6444)
## Summary

In https://github.com/astral-sh/ruff/pull/6397, the documentation was
updated stating that the default target-version is now "py38", but the
actual default value wasn't updated and remained py310. This commit
updates the default value to match what the documentation says.
2023-08-09 12:08:47 +00:00
Micha Reiser
a39dd76d95 Add enter and leave_node methods to Preoder visitor (#6422) 2023-08-09 09:09:00 +00:00
Dhruv Manilawala
e257c5af32 Add support for help end IPython escape commands (#6358)
## Summary

This PR adds support for a stricter version of help end escape
commands[^1] in the parser. By stricter, I mean that the escape tokens
are only at the end of the command and there are no tokens at the start.
This makes it difficult to implement it in the lexer without having to
do a lot of look aheads or keeping track of previous tokens.

Now, as we're adding this in the parser, the lexer needs to recognize
and emit a new token for `?`. So, `Question` token is added which will
be recognized only in `Jupyter` mode.

The conditions applied are the same as the ones in the original
implementation in IPython codebase (which is a regex):
* There can only be either 1 or 2 question mark(s) at the end
* The node before the question mark can be a `Name`, `Attribute`,
`Subscript` (only with integer constants in slice position), or any
combination of the 3 nodes.

## Test Plan

Added test cases for various combination of the possible nodes in the
command value position and update the snapshots.

fixes: #6359
fixes: #5030 (This is the final piece)

[^1]: https://github.com/astral-sh/ruff/pull/6272#issue-1833094281
2023-08-09 10:28:52 +05:30
Dhruv Manilawala
887a47cad9 Avoid S108 if path is inside tempfile.* call (#6416) 2023-08-09 10:22:31 +05:30
Charlie Marsh
a2758513de Fix false-positive in submodule resolution (#6435)
Closes https://github.com/astral-sh/ruff/issues/6433.
2023-08-09 02:36:39 +00:00
Tom Kuson
1b9fed8397 Error on zero tab width (#6429)
## Summary

Error if `tab-size` is set to zero (it is used as a divisor). Closes
#6423.

Also fixes a typo.

## Test Plan

Running ruff with a config

```toml
[tool.ruff]
tab-size = 0
```

returns an error message to the user saying that `tab-size` must be
greater than zero.
2023-08-08 16:51:37 -04:00
Charlie Marsh
55d6fd53cd Treat comments on open parentheses in return annotations as dangling (#6413)
## Summary

Given:

```python
def double(a: int) -> ( # Hello
    int
):
    return 2*a
```

We currently treat `# Hello` as a trailing comment on the parameters
(`(a: int)`). This PR adds a placement method to instead treat it as a
dangling comment on the function definition itself, so that it gets
formatted at the end of the definition, like:

```python
def double(a: int) -> int:  # Hello
    return 2*a
```

The formatting in this case is unchanged, but it's incorrect IMO for
that to be a trailing comment on the parameters, and that placement
leads to an instability after changing the grouping in #6410.

Fixing this led to a _different_ instability related to tuple return
type annotations, like:

```python
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> (  # type: ignore[override]
):
    ...
```

(This is a real example.)

To fix, I had to special-case tuples in that spot, though I'm not
certain that's correct.
2023-08-08 16:48:38 -04:00
Zanie Blue
d33618062e Improve documentation for PLE1300 (#6430) 2023-08-08 20:16:36 +00:00
Charlie Marsh
c7703e205d Move empty_parenthesized into the parentheses.rs (#6403)
## Summary

This PR moves `empty_parenthesized` such that it's peer to
`parenthesized`, and changes the API to better match that of
`parenthesized` (takes `&str` rather than `StaticText`, has a
`with_dangling_comments` method, etc.).

It may be intentionally _not_ part of `parentheses.rs`, but to me
they're so similar that it makes more sense for them to be in the same
module, with the same API, etc.
2023-08-08 19:17:17 +00:00
81 changed files with 42147 additions and 36237 deletions

View File

@@ -40,7 +40,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@2.0.0
uses: cloudflare/wrangler-action@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@2.0.0
uses: cloudflare/wrangler-action@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -1,6 +1,6 @@
# Breaking Changes
## 0.0.283
## 0.0.283 / 0.284
### The target Python version now defaults to 3.8 instead of 3.10 ([#6397](https://github.com/astral-sh/ruff/pull/6397))
@@ -8,6 +8,8 @@ Previously, when a target Python version was not specified, Ruff would use a def
(We still support Python 3.7 but since [it has reached EOL](https://devguide.python.org/versions/#unsupported-versions) we've decided not to make it the default here.)
Note this change was announced in 0.0.283 but not active until 0.0.284.
## 0.0.277
### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

6
Cargo.lock generated
View File

@@ -800,7 +800,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.283"
version = "0.0.284"
dependencies = [
"anyhow",
"clap",
@@ -2042,7 +2042,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.283"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2141,7 +2141,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.283"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.283
rev: v0.0.284
hooks:
- id: ruff
```

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.283"
version = "0.0.284"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.283"
version = "0.0.284"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -14,3 +14,19 @@ with open("/dev/shm/unit/test", "w") as f:
# not ok by config
with open("/foo/bar", "w") as f:
f.write("def")
# Using `tempfile` module should be ok
import tempfile
from tempfile import TemporaryDirectory
with tempfile.NamedTemporaryFile(dir="/tmp") as f:
f.write(b"def")
with tempfile.NamedTemporaryFile(dir="/var/tmp") as f:
f.write(b"def")
with tempfile.TemporaryDirectory(dir="/dev/shm") as d:
pass
with TemporaryDirectory(dir="/tmp") as d:
pass

View File

@@ -92,3 +92,10 @@ match *0, 1, *2:
case 0,:
import x
import y
# Test: access a sub-importation via an alias.
import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))

View File

@@ -70,3 +70,13 @@ import requests_mock as rm
def requests_mock(requests_mock: rm.Mocker):
print(rm.ANY)
import sklearn.base
import mlflow.sklearn
def f():
import sklearn
mlflow

View File

@@ -1229,13 +1229,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::HardcodedTempFile) {
if let Some(diagnostic) = flake8_bandit::rules::hardcoded_tmp_directory(
expr,
value,
&checker.settings.flake8_bandit.hardcoded_tmp_directory,
) {
checker.diagnostics.push(diagnostic);
}
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());

View File

@@ -571,11 +571,11 @@ print("after empty cells")
}
#[test]
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
fn test_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("line_magics_expected.ipynb"),
Path::new("ipy_escape_command_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |

View File

@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU8;
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
@@ -83,7 +84,7 @@ impl LineWidth {
}
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
let tab_size: usize = self.tab_size.into();
let tab_size: usize = self.tab_size.as_usize();
for c in chars {
match c {
'\t' => {
@@ -144,22 +145,22 @@ impl PartialOrd<LineLength> for LineWidth {
/// The size of a tab.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabSize(pub u8);
pub struct TabSize(pub NonZeroU8);
impl TabSize {
fn as_usize(self) -> usize {
self.0.get() as usize
}
}
impl Default for TabSize {
fn default() -> Self {
Self(4)
Self(NonZeroU8::new(4).unwrap())
}
}
impl From<u8> for TabSize {
fn from(tab_size: u8) -> Self {
impl From<NonZeroU8> for TabSize {
fn from(tab_size: NonZeroU8) -> Self {
Self(tab_size)
}
}
impl From<TabSize> for usize {
fn from(tab_size: TabSize) -> Self {
tab_size.0 as usize
}
}

View File

@@ -293,12 +293,10 @@ impl Display for MessageCodeFrame<'_> {
}
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: TabSize = TabSize(4); // TODO(jonathan): use `tab-size`
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidth::new(TAB_SIZE);
let mut line_width = LineWidth::new(TabSize::default());
for (index, c) in source.char_indices() {
let old_width = line_width.get();

View File

@@ -1,8 +1,10 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the use of hardcoded temporary file or directory paths.
///
@@ -49,19 +51,33 @@ impl Violation for HardcodedTempFile {
}
/// S108
pub(crate) fn hardcoded_tmp_directory(
expr: &Expr,
value: &str,
prefixes: &[String],
) -> Option<Diagnostic> {
if prefixes.iter().any(|prefix| value.starts_with(prefix)) {
Some(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
))
} else {
None
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, expr: &Expr, value: &str) {
if !checker
.settings
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| value.starts_with(prefix))
{
return;
}
if let Some(Expr::Call(ast::ExprCall { func, .. })) =
checker.semantic().current_expression_parent()
{
if checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["tempfile", ..]))
{
return;
}
}
checker.diagnostics.push(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
));
}

View File

@@ -49,7 +49,6 @@ mod tests {
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -60,6 +59,17 @@ mod tests {
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";
let diagnostics = test_path(
Path::new("flake8_bugbear").join(snapshot).as_path(),
&Settings::for_rule(Rule::ZipWithoutExplicitStrict),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
@@ -72,7 +82,7 @@ mod tests {
"fastapi.Query".to_string(),
],
},
..Settings::for_rules(vec![Rule::FunctionCallInDefaultArgument])
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)
},
)?;
assert_messages!(snapshot, diagnostics);

View File

@@ -1,4 +1,25 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI050.py:13:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
13 | def foo_no_return(arg: NoReturn):
| ^^^^^^^^ PYI050
14 | ...
|
PYI050.py:23:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
23 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn):
| ^^^^^^^^ PYI050
24 | ...
|
PYI050.py:27:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
27 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn):
| ^^^^^^^^ PYI050
28 | ...
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI050.pyi:6:24: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for argument annotations
PYI050.pyi:6:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
4 | def foo(arg): ...
5 | def foo_int(arg: int): ...
@@ -11,7 +11,7 @@ PYI050.pyi:6:24: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for arg
8 | arg: typing_extensions.NoReturn,
|
PYI050.pyi:10:44: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for argument annotations
PYI050.pyi:10:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
8 | arg: typing_extensions.NoReturn,
9 | ): ... # Error: PYI050
@@ -21,7 +21,7 @@ PYI050.pyi:10:44: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for ar
12 | def foo_never(arg: Never): ...
|
PYI050.pyi:11:47: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for argument annotations
PYI050.pyi:11:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
9 | ): ... # Error: PYI050
10 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050

View File

@@ -14,9 +14,8 @@ if_elif_else.py:6:1: I001 [*] Import block is un-sorted or un-formatted
3 3 | elif "setuptools" in sys.modules:
4 4 | from setuptools.command.sdist import sdist as _sdist
5 5 | else:
6 |- from setuptools.command.sdist import sdist as _sdist
7 6 | from distutils.command.sdist import sdist as _sdist
7 |+
8 |+ from setuptools.command.sdist import sdist as _sdist
6 |+ from distutils.command.sdist import sdist as _sdist
6 7 | from setuptools.command.sdist import sdist as _sdist
7 |- from distutils.command.sdist import sdist as _sdist

View File

@@ -10,6 +10,7 @@ mod tests {
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PythonVersion;
use crate::settings::Settings;
use crate::test::test_path;
@@ -22,7 +23,7 @@ mod tests {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("perflint").join(path).as_path(),
&Settings::for_rule(rule_code),
&Settings::for_rule(rule_code).with_target_version(PythonVersion::Py310),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())

View File

@@ -6,6 +6,7 @@ pub(crate) mod helpers;
#[cfg(test)]
mod tests {
use std::num::NonZeroU8;
use std::path::Path;
use anyhow::Result;
@@ -204,7 +205,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pycodestyle/E501_2.py"),
&settings::Settings {
tab_size: tab_size.into(),
tab_size: NonZeroU8::new(tab_size).unwrap().into(),
..settings::Settings::for_rule(Rule::LineTooLong)
},
)?;

View File

@@ -151,6 +151,8 @@ F401_0.py:93:16: F401 [*] `x` imported but unused
92 92 | case 0,:
93 |- import x
94 93 | import y
95 94 |
96 95 |
F401_0.py:94:16: F401 [*] `y` imported but unused
|
@@ -166,5 +168,27 @@ F401_0.py:94:16: F401 [*] `y` imported but unused
92 92 | case 0,:
93 93 | import x
94 |- import y
95 94 |
96 95 |
97 96 | # Test: access a sub-importation via an alias.
F401_0.py:99:8: F401 [*] `foo.bar.baz` imported but unused
|
97 | # Test: access a sub-importation via an alias.
98 | import foo.bar as bop
99 | import foo.bar.baz
| ^^^^^^^^^^^ F401
100 |
101 | print(bop.baz.read_csv("test.csv"))
|
= help: Remove unused import: `foo.bar.baz`
Fix
96 96 |
97 97 | # Test: access a sub-importation via an alias.
98 98 | import foo.bar as bop
99 |-import foo.bar.baz
100 99 |
101 100 | print(bop.baz.read_csv("test.csv"))

View File

@@ -130,7 +130,7 @@ mod tests {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("pylint").join(path).as_path(),
&Settings::for_rules(vec![rule_code]),
&Settings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
@@ -140,10 +140,8 @@ mod tests {
fn repeated_isinstance_calls() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/repeated_isinstance_calls.py"),
&Settings {
target_version: PythonVersion::Py39,
..Settings::for_rules(vec![Rule::RepeatedIsinstanceCalls])
},
&Settings::for_rule(Rule::RepeatedIsinstanceCalls)
.with_target_version(PythonVersion::Py39),
)?;
assert_messages!(diagnostics);
Ok(())
@@ -153,10 +151,7 @@ mod tests {
fn continue_in_finally() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/continue_in_finally.py"),
&Settings {
target_version: PythonVersion::Py37,
..Settings::for_rules(vec![Rule::ContinueInFinally])
},
&Settings::for_rule(Rule::ContinueInFinally).with_target_version(PythonVersion::Py37),
)?;
assert_messages!(diagnostics);
Ok(())
@@ -171,7 +166,7 @@ mod tests {
allow_magic_value_types: vec![pylint::settings::ConstantType::Int],
..pylint::settings::Settings::default()
},
..Settings::for_rules(vec![Rule::MagicValueComparison])
..Settings::for_rule(Rule::MagicValueComparison)
},
)?;
assert_messages!(diagnostics);
@@ -187,7 +182,7 @@ mod tests {
max_args: 4,
..pylint::settings::Settings::default()
},
..Settings::for_rules(vec![Rule::TooManyArguments])
..Settings::for_rule(Rule::TooManyArguments)
},
)?;
assert_messages!(diagnostics);
@@ -200,7 +195,7 @@ mod tests {
Path::new("pylint/too_many_arguments_params.py"),
&Settings {
dummy_variable_rgx: Regex::new(r"skip_.*").unwrap(),
..Settings::for_rules(vec![Rule::TooManyArguments])
..Settings::for_rule(Rule::TooManyArguments)
},
)?;
assert_messages!(diagnostics);
@@ -216,7 +211,7 @@ mod tests {
max_branches: 1,
..pylint::settings::Settings::default()
},
..Settings::for_rules(vec![Rule::TooManyBranches])
..Settings::for_rule(Rule::TooManyBranches)
},
)?;
assert_messages!(diagnostics);
@@ -232,7 +227,7 @@ mod tests {
max_statements: 1,
..pylint::settings::Settings::default()
},
..Settings::for_rules(vec![Rule::TooManyStatements])
..Settings::for_rule(Rule::TooManyStatements)
},
)?;
assert_messages!(diagnostics);
@@ -248,7 +243,7 @@ mod tests {
max_returns: 1,
..pylint::settings::Settings::default()
},
..Settings::for_rules(vec![Rule::TooManyReturnStatements])
..Settings::for_rule(Rule::TooManyReturnStatements)
},
)?;
assert_messages!(diagnostics);

View File

@@ -19,8 +19,7 @@ use crate::checkers::ast::Checker;
/// Checks for unsupported format types in format strings.
///
/// ## Why is this bad?
/// The format string is not checked at compile time, so it is easy to
/// introduce bugs by mistyping the format string.
/// An invalid format string character will result in an error at runtime.
///
/// ## Example
/// ```python
@@ -41,6 +40,7 @@ impl Violation for BadStringFormatCharacter {
}
}
/// PLE1300
/// Ex) `"{:z}".format("1")`
pub(crate) fn call(checker: &mut Checker, string: &str, range: TextRange) {
if let Ok(format_string) = FormatString::from_str(string) {
@@ -64,6 +64,7 @@ pub(crate) fn call(checker: &mut Checker, string: &str, range: TextRange) {
}
}
/// PLE1300
/// Ex) `"%z" % "1"`
pub(crate) fn percent(checker: &mut Checker, expr: &Expr) {
// Grab each string segment (in case there's an implicit concatenation).

View File

@@ -77,6 +77,7 @@ mod tests {
#[test_case(Rule::UselessObjectInheritance, Path::new("UP004.py"))]
#[test_case(Rule::YieldInForLoop, Path::new("UP028_0.py"))]
#[test_case(Rule::YieldInForLoop, Path::new("UP028_1.py"))]
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().to_string();
let diagnostics = test_path(
@@ -100,19 +101,6 @@ mod tests {
Ok(())
}
#[test]
fn non_pep695_type_alias_py312() -> Result<()> {
let diagnostics = test_path(
Path::new("pyupgrade/UP040.py"),
&settings::Settings {
target_version: PythonVersion::Py312,
..settings::Settings::for_rule(Rule::NonPEP695TypeAlias)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn future_annotations_keep_runtime_typing_p37() -> Result<()> {
let diagnostics = test_path(

View File

@@ -130,16 +130,18 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Name(name) if name.ctx.is_load() => {
let Some(Stmt::Assign(StmtAssign { value, .. })) =
self.semantic.lookup_symbol(name.id.as_str())
.and_then(|binding_id| {
self.semantic
.binding(binding_id)
.source
.map(|node_id| self.semantic.statement(node_id))
}) else {
return;
};
let Some(Stmt::Assign(StmtAssign { value, .. })) = self
.semantic
.lookup_symbol(name.id.as_str())
.and_then(|binding_id| {
self.semantic
.binding(binding_id)
.source
.map(|node_id| self.semantic.statement(node_id))
})
else {
return;
};
match value.as_ref() {
Expr::Subscript(ExprSubscript {

View File

@@ -974,4 +974,19 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
78 78 | # OK
79 79 | from a import b
UP035.py:88:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
|
87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
88 | from typing_extensions import dataclass_transform
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
= help: Import from `typing`
Suggested fix
85 85 | from typing_extensions import NamedTuple
86 86 |
87 87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
88 |-from typing_extensions import dataclass_transform
88 |+from typing import dataclass_transform

View File

@@ -60,10 +60,8 @@ mod tests {
);
let diagnostics = test_path(
Path::new("ruff").join(path).as_path(),
&settings::Settings {
target_version: PythonVersion::Py39,
..settings::Settings::for_rule(Rule::ImplicitOptional)
},
&settings::Settings::for_rule(Rule::ImplicitOptional)
.with_target_version(PythonVersion::Py39),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())

View File

@@ -653,13 +653,13 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Expr::Await(_)
| Expr::Yield(_)
| Expr::YieldFrom(_) => self.unconditional_next_block(after),
Expr::LineMagic(_) => todo!(),
Expr::IpyEscapeCommand(_) => todo!(),
}
}
// The tough branches are done, here is an easy one.
Stmt::Return(_) => NextBlock::Terminate,
Stmt::TypeAlias(_) => todo!(),
Stmt::LineMagic(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
};
// Include any statements in the block that don't divert the control flow.
@@ -903,7 +903,7 @@ fn needs_next_block(stmts: &[Stmt]) -> bool {
| Stmt::TryStar(_)
| Stmt::Assert(_) => true,
Stmt::TypeAlias(_) => todo!(),
Stmt::LineMagic(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}
@@ -936,7 +936,7 @@ fn is_control_flow_stmt(stmt: &Stmt) -> bool {
| Stmt::Break(_)
| Stmt::Continue(_) => true,
Stmt::TypeAlias(_) => todo!(),
Stmt::LineMagic(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}

View File

@@ -24,8 +24,6 @@ pub const PREFIXES: &[RuleSelector] = &[
RuleSelector::Linter(Linter::Pyflakes),
];
pub const TARGET_VERSION: PythonVersion = PythonVersion::Py310;
pub const TASK_TAGS: &[&str] = &["TODO", "FIXME", "XXX"];
pub static DUMMY_VARIABLE_RGX: Lazy<Regex> =
@@ -91,7 +89,7 @@ impl Default for Settings {
respect_gitignore: true,
src: vec![path_dedot::CWD.clone()],
tab_size: TabSize::default(),
target_version: TARGET_VERSION,
target_version: PythonVersion::default(),
task_tags: TASK_TAGS.iter().map(ToString::to_string).collect(),
typing_modules: vec![],
flake8_annotations: flake8_annotations::settings::Settings::default(),

View File

@@ -183,7 +183,7 @@ impl Settings {
.src
.unwrap_or_else(|| vec![project_root.to_path_buf()]),
project_root: project_root.to_path_buf(),
target_version: config.target_version.unwrap_or(defaults::TARGET_VERSION),
target_version: config.target_version.unwrap_or_default(),
task_tags: config.task_tags.unwrap_or_else(|| {
defaults::TASK_TAGS
.iter()
@@ -298,6 +298,7 @@ impl Settings {
pub fn for_rule(rule_code: Rule) -> Self {
Self {
rules: RuleTable::from_iter([rule_code]),
target_version: PythonVersion::latest(),
..Self::default()
}
}
@@ -305,9 +306,17 @@ impl Settings {
pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self {
Self {
rules: RuleTable::from_iter(rules),
target_version: PythonVersion::latest(),
..Self::default()
}
}
/// Return the [`Settings`] after updating the target [`PythonVersion`].
#[must_use]
pub fn with_target_version(mut self, target_version: PythonVersion) -> Self {
self.target_version = target_version;
self
}
}
impl From<&Configuration> for RuleTable {

View File

@@ -312,13 +312,13 @@ pub struct Options {
"#
)]
/// The line length to use when enforcing long-lines violations (like
/// `E501`).
/// `E501`). Must be greater than `0`.
pub line_length: Option<LineLength>,
#[option(
default = "4",
value_type = "int",
example = r#"
tab_size = 8
tab-size = 8
"#
)]
/// The tabulation size to calculate line length.
@@ -456,7 +456,7 @@ pub struct Options {
/// contained an `__init__.py` file.
pub namespace_packages: Option<Vec<String>>,
#[option(
default = r#""py310""#,
default = r#""py38""#,
value_type = r#""py37" | "py38" | "py39" | "py310" | "py311" | "py312""#,
example = r#"
# Always generate Python 3.7-compatible code.

View File

@@ -19,13 +19,25 @@ use crate::registry::RuleSet;
use crate::rule_selector::RuleSelector;
#[derive(
Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, CacheKey, EnumIter,
Clone,
Copy,
Debug,
PartialOrd,
Ord,
PartialEq,
Eq,
Default,
Serialize,
Deserialize,
CacheKey,
EnumIter,
)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PythonVersion {
Py37,
#[default]
Py38,
Py39,
Py310,
@@ -41,6 +53,11 @@ impl From<PythonVersion> for Pep440Version {
}
impl PythonVersion {
/// Return the latest supported Python version.
pub const fn latest() -> Self {
Self::Py312
}
pub const fn as_tuple(&self) -> (u32, u32) {
match self {
Self::Py37 => (3, 7),

View File

@@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::num::NonZeroU8;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
@@ -205,6 +206,13 @@ impl CacheKey for i8 {
}
}
impl CacheKey for NonZeroU8 {
#[inline]
fn cache_key(&self, state: &mut CacheKeyHasher) {
state.write_u8(self.get());
}
}
macro_rules! impl_cache_key_tuple {
() => (
impl CacheKey for () {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.283"
version = "0.0.284"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -672,8 +672,8 @@ pub struct ExprSlice<'a> {
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprLineMagic<'a> {
kind: ast::MagicKind,
pub struct ExprIpyEscapeCommand<'a> {
kind: ast::IpyEscapeKind,
value: &'a str,
}
@@ -706,7 +706,7 @@ pub enum ComparableExpr<'a> {
List(ExprList<'a>),
Tuple(ExprTuple<'a>),
Slice(ExprSlice<'a>),
LineMagic(ExprLineMagic<'a>),
IpyEscapeCommand(ExprIpyEscapeCommand<'a>),
}
impl<'a> From<&'a Box<ast::Expr>> for Box<ComparableExpr<'a>> {
@@ -936,11 +936,11 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
upper: upper.as_ref().map(Into::into),
step: step.as_ref().map(Into::into),
}),
ast::Expr::LineMagic(ast::ExprLineMagic {
ast::Expr::IpyEscapeCommand(ast::ExprIpyEscapeCommand {
kind,
value,
range: _,
}) => Self::LineMagic(ExprLineMagic {
}) => Self::IpyEscapeCommand(ExprIpyEscapeCommand {
kind: *kind,
value: value.as_str(),
}),
@@ -1165,8 +1165,8 @@ pub struct StmtExpr<'a> {
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct StmtLineMagic<'a> {
kind: ast::MagicKind,
pub struct StmtIpyEscapeCommand<'a> {
kind: ast::IpyEscapeKind,
value: &'a str,
}
@@ -1193,7 +1193,7 @@ pub enum ComparableStmt<'a> {
ImportFrom(StmtImportFrom<'a>),
Global(StmtGlobal<'a>),
Nonlocal(StmtNonlocal<'a>),
LineMagic(StmtLineMagic<'a>),
IpyEscapeCommand(StmtIpyEscapeCommand<'a>),
Expr(StmtExpr<'a>),
Pass,
Break,
@@ -1394,11 +1394,11 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> {
names: names.iter().map(ast::Identifier::as_str).collect(),
})
}
ast::Stmt::LineMagic(ast::StmtLineMagic {
ast::Stmt::IpyEscapeCommand(ast::StmtIpyEscapeCommand {
kind,
value,
range: _,
}) => Self::LineMagic(StmtLineMagic {
}) => Self::IpyEscapeCommand(StmtIpyEscapeCommand {
kind: *kind,
value: value.as_str(),
}),

View File

@@ -108,7 +108,7 @@ where
| Expr::Subscript(_)
| Expr::Yield(_)
| Expr::YieldFrom(_)
| Expr::LineMagic(_)
| Expr::IpyEscapeCommand(_)
)
})
}
@@ -247,7 +247,7 @@ where
.is_some_and(|value| any_over_expr(value, func))
}
Expr::Name(_) | Expr::Constant(_) => false,
Expr::LineMagic(_) => false,
Expr::IpyEscapeCommand(_) => false,
}
}
@@ -534,7 +534,7 @@ where
Stmt::Nonlocal(_) => false,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => any_over_expr(value, func),
Stmt::Pass(_) | Stmt::Break(_) | Stmt::Continue(_) => false,
Stmt::LineMagic(_) => false,
Stmt::IpyEscapeCommand(_) => false,
}
}

View File

@@ -48,7 +48,7 @@ pub enum AnyNode {
StmtPass(ast::StmtPass),
StmtBreak(ast::StmtBreak),
StmtContinue(ast::StmtContinue),
StmtLineMagic(ast::StmtLineMagic),
StmtIpyEscapeCommand(ast::StmtIpyEscapeCommand),
ExprBoolOp(ast::ExprBoolOp),
ExprNamedExpr(ast::ExprNamedExpr),
ExprBinOp(ast::ExprBinOp),
@@ -76,7 +76,7 @@ pub enum AnyNode {
ExprList(ast::ExprList),
ExprTuple(ast::ExprTuple),
ExprSlice(ast::ExprSlice),
ExprLineMagic(ast::ExprLineMagic),
ExprIpyEscapeCommand(ast::ExprIpyEscapeCommand),
ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler),
PatternMatchValue(ast::PatternMatchValue),
PatternMatchSingleton(ast::PatternMatchSingleton),
@@ -131,7 +131,7 @@ impl AnyNode {
AnyNode::StmtPass(node) => Some(Stmt::Pass(node)),
AnyNode::StmtBreak(node) => Some(Stmt::Break(node)),
AnyNode::StmtContinue(node) => Some(Stmt::Continue(node)),
AnyNode::StmtLineMagic(node) => Some(Stmt::LineMagic(node)),
AnyNode::StmtIpyEscapeCommand(node) => Some(Stmt::IpyEscapeCommand(node)),
AnyNode::ModModule(_)
| AnyNode::ModExpression(_)
@@ -162,7 +162,7 @@ impl AnyNode {
| AnyNode::ExprList(_)
| AnyNode::ExprTuple(_)
| AnyNode::ExprSlice(_)
| AnyNode::ExprLineMagic(_)
| AnyNode::ExprIpyEscapeCommand(_)
| AnyNode::ExceptHandlerExceptHandler(_)
| AnyNode::PatternMatchValue(_)
| AnyNode::PatternMatchSingleton(_)
@@ -219,7 +219,7 @@ impl AnyNode {
AnyNode::ExprList(node) => Some(Expr::List(node)),
AnyNode::ExprTuple(node) => Some(Expr::Tuple(node)),
AnyNode::ExprSlice(node) => Some(Expr::Slice(node)),
AnyNode::ExprLineMagic(node) => Some(Expr::LineMagic(node)),
AnyNode::ExprIpyEscapeCommand(node) => Some(Expr::IpyEscapeCommand(node)),
AnyNode::ModModule(_)
| AnyNode::ModExpression(_)
@@ -248,7 +248,7 @@ impl AnyNode {
| AnyNode::StmtPass(_)
| AnyNode::StmtBreak(_)
| AnyNode::StmtContinue(_)
| AnyNode::StmtLineMagic(_)
| AnyNode::StmtIpyEscapeCommand(_)
| AnyNode::ExceptHandlerExceptHandler(_)
| AnyNode::PatternMatchValue(_)
| AnyNode::PatternMatchSingleton(_)
@@ -306,7 +306,7 @@ impl AnyNode {
| AnyNode::StmtPass(_)
| AnyNode::StmtBreak(_)
| AnyNode::StmtContinue(_)
| AnyNode::StmtLineMagic(_)
| AnyNode::StmtIpyEscapeCommand(_)
| AnyNode::ExprBoolOp(_)
| AnyNode::ExprNamedExpr(_)
| AnyNode::ExprBinOp(_)
@@ -334,7 +334,7 @@ impl AnyNode {
| AnyNode::ExprList(_)
| AnyNode::ExprTuple(_)
| AnyNode::ExprSlice(_)
| AnyNode::ExprLineMagic(_)
| AnyNode::ExprIpyEscapeCommand(_)
| AnyNode::ExceptHandlerExceptHandler(_)
| AnyNode::PatternMatchValue(_)
| AnyNode::PatternMatchSingleton(_)
@@ -400,7 +400,7 @@ impl AnyNode {
| AnyNode::StmtPass(_)
| AnyNode::StmtBreak(_)
| AnyNode::StmtContinue(_)
| AnyNode::StmtLineMagic(_)
| AnyNode::StmtIpyEscapeCommand(_)
| AnyNode::ExprBoolOp(_)
| AnyNode::ExprNamedExpr(_)
| AnyNode::ExprBinOp(_)
@@ -428,7 +428,7 @@ impl AnyNode {
| AnyNode::ExprList(_)
| AnyNode::ExprTuple(_)
| AnyNode::ExprSlice(_)
| AnyNode::ExprLineMagic(_)
| AnyNode::ExprIpyEscapeCommand(_)
| AnyNode::ExceptHandlerExceptHandler(_)
| AnyNode::Comprehension(_)
| AnyNode::Arguments(_)
@@ -479,7 +479,7 @@ impl AnyNode {
| AnyNode::StmtPass(_)
| AnyNode::StmtBreak(_)
| AnyNode::StmtContinue(_)
| AnyNode::StmtLineMagic(_)
| AnyNode::StmtIpyEscapeCommand(_)
| AnyNode::ExprBoolOp(_)
| AnyNode::ExprNamedExpr(_)
| AnyNode::ExprBinOp(_)
@@ -507,7 +507,7 @@ impl AnyNode {
| AnyNode::ExprList(_)
| AnyNode::ExprTuple(_)
| AnyNode::ExprSlice(_)
| AnyNode::ExprLineMagic(_)
| AnyNode::ExprIpyEscapeCommand(_)
| AnyNode::PatternMatchValue(_)
| AnyNode::PatternMatchSingleton(_)
| AnyNode::PatternMatchSequence(_)
@@ -583,7 +583,7 @@ impl AnyNode {
Self::StmtPass(node) => AnyNodeRef::StmtPass(node),
Self::StmtBreak(node) => AnyNodeRef::StmtBreak(node),
Self::StmtContinue(node) => AnyNodeRef::StmtContinue(node),
Self::StmtLineMagic(node) => AnyNodeRef::StmtLineMagic(node),
Self::StmtIpyEscapeCommand(node) => AnyNodeRef::StmtIpyEscapeCommand(node),
Self::ExprBoolOp(node) => AnyNodeRef::ExprBoolOp(node),
Self::ExprNamedExpr(node) => AnyNodeRef::ExprNamedExpr(node),
Self::ExprBinOp(node) => AnyNodeRef::ExprBinOp(node),
@@ -611,7 +611,7 @@ impl AnyNode {
Self::ExprList(node) => AnyNodeRef::ExprList(node),
Self::ExprTuple(node) => AnyNodeRef::ExprTuple(node),
Self::ExprSlice(node) => AnyNodeRef::ExprSlice(node),
Self::ExprLineMagic(node) => AnyNodeRef::ExprLineMagic(node),
Self::ExprIpyEscapeCommand(node) => AnyNodeRef::ExprIpyEscapeCommand(node),
Self::ExceptHandlerExceptHandler(node) => AnyNodeRef::ExceptHandlerExceptHandler(node),
Self::PatternMatchValue(node) => AnyNodeRef::PatternMatchValue(node),
Self::PatternMatchSingleton(node) => AnyNodeRef::PatternMatchSingleton(node),
@@ -1429,12 +1429,12 @@ impl AstNode for ast::StmtContinue {
AnyNode::from(self)
}
}
impl AstNode for ast::StmtLineMagic {
impl AstNode for ast::StmtIpyEscapeCommand {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::StmtLineMagic(node) = kind {
if let AnyNode::StmtIpyEscapeCommand(node) = kind {
Some(node)
} else {
None
@@ -1442,7 +1442,7 @@ impl AstNode for ast::StmtLineMagic {
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::StmtLineMagic(node) = kind {
if let AnyNodeRef::StmtIpyEscapeCommand(node) = kind {
Some(node)
} else {
None
@@ -2213,12 +2213,12 @@ impl AstNode for ast::ExprSlice {
AnyNode::from(self)
}
}
impl AstNode for ast::ExprLineMagic {
impl AstNode for ast::ExprIpyEscapeCommand {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::ExprLineMagic(node) = kind {
if let AnyNode::ExprIpyEscapeCommand(node) = kind {
Some(node)
} else {
None
@@ -2226,7 +2226,7 @@ impl AstNode for ast::ExprLineMagic {
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::ExprLineMagic(node) = kind {
if let AnyNodeRef::ExprIpyEscapeCommand(node) = kind {
Some(node)
} else {
None
@@ -2915,7 +2915,7 @@ impl From<Stmt> for AnyNode {
Stmt::Pass(node) => AnyNode::StmtPass(node),
Stmt::Break(node) => AnyNode::StmtBreak(node),
Stmt::Continue(node) => AnyNode::StmtContinue(node),
Stmt::LineMagic(node) => AnyNode::StmtLineMagic(node),
Stmt::IpyEscapeCommand(node) => AnyNode::StmtIpyEscapeCommand(node),
}
}
}
@@ -2950,7 +2950,7 @@ impl From<Expr> for AnyNode {
Expr::List(node) => AnyNode::ExprList(node),
Expr::Tuple(node) => AnyNode::ExprTuple(node),
Expr::Slice(node) => AnyNode::ExprSlice(node),
Expr::LineMagic(node) => AnyNode::ExprLineMagic(node),
Expr::IpyEscapeCommand(node) => AnyNode::ExprIpyEscapeCommand(node),
}
}
}
@@ -3155,9 +3155,9 @@ impl From<ast::StmtContinue> for AnyNode {
}
}
impl From<ast::StmtLineMagic> for AnyNode {
fn from(node: ast::StmtLineMagic) -> Self {
AnyNode::StmtLineMagic(node)
impl From<ast::StmtIpyEscapeCommand> for AnyNode {
fn from(node: ast::StmtIpyEscapeCommand) -> Self {
AnyNode::StmtIpyEscapeCommand(node)
}
}
@@ -3323,9 +3323,9 @@ impl From<ast::ExprSlice> for AnyNode {
}
}
impl From<ast::ExprLineMagic> for AnyNode {
fn from(node: ast::ExprLineMagic) -> Self {
AnyNode::ExprLineMagic(node)
impl From<ast::ExprIpyEscapeCommand> for AnyNode {
fn from(node: ast::ExprIpyEscapeCommand) -> Self {
AnyNode::ExprIpyEscapeCommand(node)
}
}
@@ -3486,7 +3486,7 @@ impl Ranged for AnyNode {
AnyNode::StmtPass(node) => node.range(),
AnyNode::StmtBreak(node) => node.range(),
AnyNode::StmtContinue(node) => node.range(),
AnyNode::StmtLineMagic(node) => node.range(),
AnyNode::StmtIpyEscapeCommand(node) => node.range(),
AnyNode::ExprBoolOp(node) => node.range(),
AnyNode::ExprNamedExpr(node) => node.range(),
AnyNode::ExprBinOp(node) => node.range(),
@@ -3514,7 +3514,7 @@ impl Ranged for AnyNode {
AnyNode::ExprList(node) => node.range(),
AnyNode::ExprTuple(node) => node.range(),
AnyNode::ExprSlice(node) => node.range(),
AnyNode::ExprLineMagic(node) => node.range(),
AnyNode::ExprIpyEscapeCommand(node) => node.range(),
AnyNode::ExceptHandlerExceptHandler(node) => node.range(),
AnyNode::PatternMatchValue(node) => node.range(),
AnyNode::PatternMatchSingleton(node) => node.range(),
@@ -3572,7 +3572,7 @@ pub enum AnyNodeRef<'a> {
StmtPass(&'a ast::StmtPass),
StmtBreak(&'a ast::StmtBreak),
StmtContinue(&'a ast::StmtContinue),
StmtLineMagic(&'a ast::StmtLineMagic),
StmtIpyEscapeCommand(&'a ast::StmtIpyEscapeCommand),
ExprBoolOp(&'a ast::ExprBoolOp),
ExprNamedExpr(&'a ast::ExprNamedExpr),
ExprBinOp(&'a ast::ExprBinOp),
@@ -3600,7 +3600,7 @@ pub enum AnyNodeRef<'a> {
ExprList(&'a ast::ExprList),
ExprTuple(&'a ast::ExprTuple),
ExprSlice(&'a ast::ExprSlice),
ExprLineMagic(&'a ast::ExprLineMagic),
ExprIpyEscapeCommand(&'a ast::ExprIpyEscapeCommand),
ExceptHandlerExceptHandler(&'a ast::ExceptHandlerExceptHandler),
PatternMatchValue(&'a ast::PatternMatchValue),
PatternMatchSingleton(&'a ast::PatternMatchSingleton),
@@ -3657,7 +3657,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::StmtPass(node) => NonNull::from(*node).cast(),
AnyNodeRef::StmtBreak(node) => NonNull::from(*node).cast(),
AnyNodeRef::StmtContinue(node) => NonNull::from(*node).cast(),
AnyNodeRef::StmtLineMagic(node) => NonNull::from(*node).cast(),
AnyNodeRef::StmtIpyEscapeCommand(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprBoolOp(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprNamedExpr(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprBinOp(node) => NonNull::from(*node).cast(),
@@ -3685,7 +3685,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ExprList(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprTuple(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprSlice(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprLineMagic(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprIpyEscapeCommand(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExceptHandlerExceptHandler(node) => NonNull::from(*node).cast(),
AnyNodeRef::PatternMatchValue(node) => NonNull::from(*node).cast(),
AnyNodeRef::PatternMatchSingleton(node) => NonNull::from(*node).cast(),
@@ -3748,7 +3748,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::StmtPass(_) => NodeKind::StmtPass,
AnyNodeRef::StmtBreak(_) => NodeKind::StmtBreak,
AnyNodeRef::StmtContinue(_) => NodeKind::StmtContinue,
AnyNodeRef::StmtLineMagic(_) => NodeKind::StmtLineMagic,
AnyNodeRef::StmtIpyEscapeCommand(_) => NodeKind::StmtIpyEscapeCommand,
AnyNodeRef::ExprBoolOp(_) => NodeKind::ExprBoolOp,
AnyNodeRef::ExprNamedExpr(_) => NodeKind::ExprNamedExpr,
AnyNodeRef::ExprBinOp(_) => NodeKind::ExprBinOp,
@@ -3776,7 +3776,7 @@ impl AnyNodeRef<'_> {
AnyNodeRef::ExprList(_) => NodeKind::ExprList,
AnyNodeRef::ExprTuple(_) => NodeKind::ExprTuple,
AnyNodeRef::ExprSlice(_) => NodeKind::ExprSlice,
AnyNodeRef::ExprLineMagic(_) => NodeKind::ExprLineMagic,
AnyNodeRef::ExprIpyEscapeCommand(_) => NodeKind::ExprIpyEscapeCommand,
AnyNodeRef::ExceptHandlerExceptHandler(_) => NodeKind::ExceptHandlerExceptHandler,
AnyNodeRef::PatternMatchValue(_) => NodeKind::PatternMatchValue,
AnyNodeRef::PatternMatchSingleton(_) => NodeKind::PatternMatchSingleton,
@@ -3831,7 +3831,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::StmtPass(_)
| AnyNodeRef::StmtBreak(_)
| AnyNodeRef::StmtContinue(_)
| AnyNodeRef::StmtLineMagic(_) => true,
| AnyNodeRef::StmtIpyEscapeCommand(_) => true,
AnyNodeRef::ModModule(_)
| AnyNodeRef::ModExpression(_)
@@ -3862,7 +3862,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprTuple(_)
| AnyNodeRef::ExprSlice(_)
| AnyNodeRef::ExprLineMagic(_)
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::ExceptHandlerExceptHandler(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
@@ -3919,7 +3919,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprTuple(_)
| AnyNodeRef::ExprSlice(_)
| AnyNodeRef::ExprLineMagic(_) => true,
| AnyNodeRef::ExprIpyEscapeCommand(_) => true,
AnyNodeRef::ModModule(_)
| AnyNodeRef::ModExpression(_)
@@ -3948,7 +3948,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::StmtPass(_)
| AnyNodeRef::StmtBreak(_)
| AnyNodeRef::StmtContinue(_)
| AnyNodeRef::StmtLineMagic(_)
| AnyNodeRef::StmtIpyEscapeCommand(_)
| AnyNodeRef::ExceptHandlerExceptHandler(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
@@ -4005,7 +4005,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::StmtPass(_)
| AnyNodeRef::StmtBreak(_)
| AnyNodeRef::StmtContinue(_)
| AnyNodeRef::StmtLineMagic(_)
| AnyNodeRef::StmtIpyEscapeCommand(_)
| AnyNodeRef::ExprBoolOp(_)
| AnyNodeRef::ExprNamedExpr(_)
| AnyNodeRef::ExprBinOp(_)
@@ -4033,7 +4033,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprTuple(_)
| AnyNodeRef::ExprSlice(_)
| AnyNodeRef::ExprLineMagic(_)
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::ExceptHandlerExceptHandler(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
@@ -4099,7 +4099,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::StmtPass(_)
| AnyNodeRef::StmtBreak(_)
| AnyNodeRef::StmtContinue(_)
| AnyNodeRef::StmtLineMagic(_)
| AnyNodeRef::StmtIpyEscapeCommand(_)
| AnyNodeRef::ExprBoolOp(_)
| AnyNodeRef::ExprNamedExpr(_)
| AnyNodeRef::ExprBinOp(_)
@@ -4127,7 +4127,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprTuple(_)
| AnyNodeRef::ExprSlice(_)
| AnyNodeRef::ExprLineMagic(_)
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::ExceptHandlerExceptHandler(_)
| AnyNodeRef::Comprehension(_)
| AnyNodeRef::Arguments(_)
@@ -4178,7 +4178,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::StmtPass(_)
| AnyNodeRef::StmtBreak(_)
| AnyNodeRef::StmtContinue(_)
| AnyNodeRef::StmtLineMagic(_)
| AnyNodeRef::StmtIpyEscapeCommand(_)
| AnyNodeRef::ExprBoolOp(_)
| AnyNodeRef::ExprNamedExpr(_)
| AnyNodeRef::ExprBinOp(_)
@@ -4206,7 +4206,7 @@ impl AnyNodeRef<'_> {
| AnyNodeRef::ExprList(_)
| AnyNodeRef::ExprTuple(_)
| AnyNodeRef::ExprSlice(_)
| AnyNodeRef::ExprLineMagic(_)
| AnyNodeRef::ExprIpyEscapeCommand(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
| AnyNodeRef::PatternMatchSequence(_)
@@ -4429,9 +4429,9 @@ impl<'a> From<&'a ast::StmtContinue> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a ast::StmtLineMagic> for AnyNodeRef<'a> {
fn from(node: &'a ast::StmtLineMagic) -> Self {
AnyNodeRef::StmtLineMagic(node)
impl<'a> From<&'a ast::StmtIpyEscapeCommand> for AnyNodeRef<'a> {
fn from(node: &'a ast::StmtIpyEscapeCommand) -> Self {
AnyNodeRef::StmtIpyEscapeCommand(node)
}
}
@@ -4597,9 +4597,9 @@ impl<'a> From<&'a ast::ExprSlice> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a ast::ExprLineMagic> for AnyNodeRef<'a> {
fn from(node: &'a ast::ExprLineMagic) -> Self {
AnyNodeRef::ExprLineMagic(node)
impl<'a> From<&'a ast::ExprIpyEscapeCommand> for AnyNodeRef<'a> {
fn from(node: &'a ast::ExprIpyEscapeCommand) -> Self {
AnyNodeRef::ExprIpyEscapeCommand(node)
}
}
@@ -4714,7 +4714,7 @@ impl<'a> From<&'a Stmt> for AnyNodeRef<'a> {
Stmt::Pass(node) => AnyNodeRef::StmtPass(node),
Stmt::Break(node) => AnyNodeRef::StmtBreak(node),
Stmt::Continue(node) => AnyNodeRef::StmtContinue(node),
Stmt::LineMagic(node) => AnyNodeRef::StmtLineMagic(node),
Stmt::IpyEscapeCommand(node) => AnyNodeRef::StmtIpyEscapeCommand(node),
}
}
}
@@ -4749,7 +4749,7 @@ impl<'a> From<&'a Expr> for AnyNodeRef<'a> {
Expr::List(node) => AnyNodeRef::ExprList(node),
Expr::Tuple(node) => AnyNodeRef::ExprTuple(node),
Expr::Slice(node) => AnyNodeRef::ExprSlice(node),
Expr::LineMagic(node) => AnyNodeRef::ExprLineMagic(node),
Expr::IpyEscapeCommand(node) => AnyNodeRef::ExprIpyEscapeCommand(node),
}
}
}
@@ -4874,7 +4874,7 @@ impl Ranged for AnyNodeRef<'_> {
AnyNodeRef::StmtPass(node) => node.range(),
AnyNodeRef::StmtBreak(node) => node.range(),
AnyNodeRef::StmtContinue(node) => node.range(),
AnyNodeRef::StmtLineMagic(node) => node.range(),
AnyNodeRef::StmtIpyEscapeCommand(node) => node.range(),
AnyNodeRef::ExprBoolOp(node) => node.range(),
AnyNodeRef::ExprNamedExpr(node) => node.range(),
AnyNodeRef::ExprBinOp(node) => node.range(),
@@ -4902,7 +4902,7 @@ impl Ranged for AnyNodeRef<'_> {
AnyNodeRef::ExprList(node) => node.range(),
AnyNodeRef::ExprTuple(node) => node.range(),
AnyNodeRef::ExprSlice(node) => node.range(),
AnyNodeRef::ExprLineMagic(node) => node.range(),
AnyNodeRef::ExprIpyEscapeCommand(node) => node.range(),
AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(),
AnyNodeRef::PatternMatchValue(node) => node.range(),
AnyNodeRef::PatternMatchSingleton(node) => node.range(),
@@ -4958,7 +4958,7 @@ pub enum NodeKind {
StmtImportFrom,
StmtGlobal,
StmtNonlocal,
StmtLineMagic,
StmtIpyEscapeCommand,
StmtExpr,
StmtPass,
StmtBreak,
@@ -4990,7 +4990,7 @@ pub enum NodeKind {
ExprList,
ExprTuple,
ExprSlice,
ExprLineMagic,
ExprIpyEscapeCommand,
ExceptHandlerExceptHandler,
PatternMatchValue,
PatternMatchSingleton,

View File

@@ -95,20 +95,20 @@ pub enum Stmt {
Continue(StmtContinue),
// Jupyter notebook specific
#[is(name = "line_magic_stmt")]
LineMagic(StmtLineMagic),
#[is(name = "ipy_escape_command_stmt")]
IpyEscapeCommand(StmtIpyEscapeCommand),
}
#[derive(Clone, Debug, PartialEq)]
pub struct StmtLineMagic {
pub struct StmtIpyEscapeCommand {
pub range: TextRange,
pub kind: MagicKind,
pub kind: IpyEscapeKind,
pub value: String,
}
impl From<StmtLineMagic> for Stmt {
fn from(payload: StmtLineMagic) -> Self {
Stmt::LineMagic(payload)
impl From<StmtIpyEscapeCommand> for Stmt {
fn from(payload: StmtIpyEscapeCommand) -> Self {
Stmt::IpyEscapeCommand(payload)
}
}
@@ -570,20 +570,20 @@ pub enum Expr {
Slice(ExprSlice),
// Jupyter notebook specific
#[is(name = "line_magic_expr")]
LineMagic(ExprLineMagic),
#[is(name = "ipy_escape_command_expr")]
IpyEscapeCommand(ExprIpyEscapeCommand),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExprLineMagic {
pub struct ExprIpyEscapeCommand {
pub range: TextRange,
pub kind: MagicKind,
pub kind: IpyEscapeKind,
pub value: String,
}
impl From<ExprLineMagic> for Expr {
fn from(payload: ExprLineMagic) -> Self {
Expr::LineMagic(payload)
impl From<ExprIpyEscapeCommand> for Expr {
fn from(payload: ExprIpyEscapeCommand) -> Self {
Expr::IpyEscapeCommand(payload)
}
}
@@ -2253,103 +2253,103 @@ impl Parameters {
}
}
/// The kind of magic command as defined in [IPython Syntax] in the IPython codebase.
/// The kind of escape command as defined in [IPython Syntax] in the IPython codebase.
///
/// [IPython Syntax]: https://github.com/ipython/ipython/blob/635815e8f1ded5b764d66cacc80bbe25e9e2587f/IPython/core/inputtransformer2.py#L335-L343
#[derive(PartialEq, Eq, Debug, Clone, Hash, Copy)]
pub enum MagicKind {
/// Send line to underlying system shell.
pub enum IpyEscapeKind {
/// Send line to underlying system shell (`!`).
Shell,
/// Send line to system shell and capture output.
/// Send line to system shell and capture output (`!!`).
ShCap,
/// Show help on object.
/// Show help on object (`?`).
Help,
/// Show help on object, with extra verbosity.
/// Show help on object, with extra verbosity (`??`).
Help2,
/// Call magic function.
/// Call magic function (`%`).
Magic,
/// Call cell magic function.
/// Call cell magic function (`%%`).
Magic2,
/// Call first argument with rest of line as arguments after splitting on whitespace
/// and quote each as string.
/// and quote each as string (`,`).
Quote,
/// Call first argument with rest of line as an argument quoted as a single string.
/// Call first argument with rest of line as an argument quoted as a single string (`;`).
Quote2,
/// Call first argument with rest of line as arguments.
/// Call first argument with rest of line as arguments (`/`).
Paren,
}
impl TryFrom<char> for MagicKind {
impl TryFrom<char> for IpyEscapeKind {
type Error = String;
fn try_from(ch: char) -> Result<Self, Self::Error> {
match ch {
'!' => Ok(MagicKind::Shell),
'?' => Ok(MagicKind::Help),
'%' => Ok(MagicKind::Magic),
',' => Ok(MagicKind::Quote),
';' => Ok(MagicKind::Quote2),
'/' => Ok(MagicKind::Paren),
'!' => Ok(IpyEscapeKind::Shell),
'?' => Ok(IpyEscapeKind::Help),
'%' => Ok(IpyEscapeKind::Magic),
',' => Ok(IpyEscapeKind::Quote),
';' => Ok(IpyEscapeKind::Quote2),
'/' => Ok(IpyEscapeKind::Paren),
_ => Err(format!("Unexpected magic escape: {ch}")),
}
}
}
impl TryFrom<[char; 2]> for MagicKind {
impl TryFrom<[char; 2]> for IpyEscapeKind {
type Error = String;
fn try_from(ch: [char; 2]) -> Result<Self, Self::Error> {
match ch {
['!', '!'] => Ok(MagicKind::ShCap),
['?', '?'] => Ok(MagicKind::Help2),
['%', '%'] => Ok(MagicKind::Magic2),
['!', '!'] => Ok(IpyEscapeKind::ShCap),
['?', '?'] => Ok(IpyEscapeKind::Help2),
['%', '%'] => Ok(IpyEscapeKind::Magic2),
[c1, c2] => Err(format!("Unexpected magic escape: {c1}{c2}")),
}
}
}
impl fmt::Display for MagicKind {
impl fmt::Display for IpyEscapeKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl MagicKind {
/// Returns the length of the magic command prefix.
impl IpyEscapeKind {
/// Returns the length of the escape kind token.
pub fn prefix_len(self) -> TextSize {
let len = match self {
MagicKind::Shell
| MagicKind::Magic
| MagicKind::Help
| MagicKind::Quote
| MagicKind::Quote2
| MagicKind::Paren => 1,
MagicKind::ShCap | MagicKind::Magic2 | MagicKind::Help2 => 2,
IpyEscapeKind::Shell
| IpyEscapeKind::Magic
| IpyEscapeKind::Help
| IpyEscapeKind::Quote
| IpyEscapeKind::Quote2
| IpyEscapeKind::Paren => 1,
IpyEscapeKind::ShCap | IpyEscapeKind::Magic2 | IpyEscapeKind::Help2 => 2,
};
len.into()
}
/// Returns `true` if the kind is a help command i.e., `?` or `??`.
/// Returns `true` if the escape kind is help i.e., `?` or `??`.
pub const fn is_help(self) -> bool {
matches!(self, MagicKind::Help | MagicKind::Help2)
matches!(self, IpyEscapeKind::Help | IpyEscapeKind::Help2)
}
/// Returns `true` if the kind is a magic command i.e., `%` or `%%`.
/// Returns `true` if the escape kind is magic i.e., `%` or `%%`.
pub const fn is_magic(self) -> bool {
matches!(self, MagicKind::Magic | MagicKind::Magic2)
matches!(self, IpyEscapeKind::Magic | IpyEscapeKind::Magic2)
}
pub fn as_str(self) -> &'static str {
match self {
MagicKind::Shell => "!",
MagicKind::ShCap => "!!",
MagicKind::Help => "?",
MagicKind::Help2 => "??",
MagicKind::Magic => "%",
MagicKind::Magic2 => "%%",
MagicKind::Quote => ",",
MagicKind::Quote2 => ";",
MagicKind::Paren => "/",
IpyEscapeKind::Shell => "!",
IpyEscapeKind::ShCap => "!!",
IpyEscapeKind::Help => "?",
IpyEscapeKind::Help2 => "??",
IpyEscapeKind::Magic => "%",
IpyEscapeKind::Magic2 => "%%",
IpyEscapeKind::Quote => ",",
IpyEscapeKind::Quote2 => ";",
IpyEscapeKind::Paren => "/",
}
}
}
@@ -2686,7 +2686,7 @@ impl Ranged for crate::nodes::StmtContinue {
self.range
}
}
impl Ranged for StmtLineMagic {
impl Ranged for StmtIpyEscapeCommand {
fn range(&self) -> TextRange {
self.range
}
@@ -2719,7 +2719,7 @@ impl Ranged for crate::Stmt {
Self::Pass(node) => node.range(),
Self::Break(node) => node.range(),
Self::Continue(node) => node.range(),
Stmt::LineMagic(node) => node.range(),
Stmt::IpyEscapeCommand(node) => node.range(),
}
}
}
@@ -2859,7 +2859,7 @@ impl Ranged for crate::nodes::ExprSlice {
self.range
}
}
impl Ranged for ExprLineMagic {
impl Ranged for ExprIpyEscapeCommand {
fn range(&self) -> TextRange {
self.range
}
@@ -2894,7 +2894,7 @@ impl Ranged for crate::Expr {
Self::List(node) => node.range(),
Self::Tuple(node) => node.range(),
Self::Slice(node) => node.range(),
Expr::LineMagic(node) => node.range(),
Expr::IpyEscapeCommand(node) => node.range(),
}
}
}

View File

@@ -199,7 +199,7 @@ pub fn relocate_expr(expr: &mut Expr, location: TextRange) {
relocate_expr(expr, location);
}
}
Expr::LineMagic(nodes::ExprLineMagic { range, .. }) => {
Expr::IpyEscapeCommand(nodes::ExprIpyEscapeCommand { range, .. }) => {
*range = location;
}
}

View File

@@ -312,7 +312,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) {
Stmt::Global(_) => {}
Stmt::Nonlocal(_) => {}
Stmt::Expr(ast::StmtExpr { value, range: _ }) => visitor.visit_expr(value),
Stmt::Pass(_) | Stmt::Break(_) | Stmt::Continue(_) | Stmt::LineMagic(_) => {}
Stmt::Pass(_) | Stmt::Break(_) | Stmt::Continue(_) | Stmt::IpyEscapeCommand(_) => {}
}
}
@@ -543,7 +543,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
visitor.visit_expr(expr);
}
}
Expr::LineMagic(_) => {}
Expr::IpyEscapeCommand(_) => {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -656,7 +656,7 @@ impl<'a> Generator<'a> {
self.p("continue");
});
}
Stmt::LineMagic(ast::StmtLineMagic { kind, value, .. }) => {
Stmt::IpyEscapeCommand(ast::StmtIpyEscapeCommand { kind, value, .. }) => {
statement!({
self.p(&format!("{kind}{value}"));
});
@@ -1184,7 +1184,7 @@ impl<'a> Generator<'a> {
self.unparse_expr(step, precedence::SLICE);
}
}
Expr::LineMagic(ast::ExprLineMagic { kind, value, .. }) => {
Expr::IpyEscapeCommand(ast::ExprIpyEscapeCommand { kind, value, .. }) => {
self.p(&format!("{kind}{value}"));
}
}

View File

@@ -371,3 +371,67 @@ def f( # first
# third
):
...
# Handle comments on empty tuple return types.
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
# comment
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
1
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
1, 2
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
(1, 2)
): ...
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
self, m: Match[str], data: str
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
...
def double(a: int # Hello
) -> (int):
return 2 * a
def double(a: int) -> ( # Hello
int
):
return 2*a
def double(a: int) -> ( # Hello
):
return 2*a
# Breaking over parameters and return types. (Black adds a trailing comma when the
# function arguments break here with a single argument; we do not.)
def f(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, a) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> a:
...
def f(a) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> a:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> a:
...

View File

@@ -3,7 +3,6 @@ use ruff_python_ast::Ranged;
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
use crate::comments::{dangling_comments, trailing_comments, SourceComment};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::prelude::*;
use crate::MagicTrailingComma;
@@ -209,64 +208,3 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
})
}
}
/// Format comments inside empty parentheses, brackets or curly braces.
///
/// Empty `()`, `[]` and `{}` are special because there can be dangling comments, and they can be in
/// two positions:
/// ```python
/// x = [ # end-of-line
/// # own line
/// ]
/// ```
/// These comments are dangling because they can't be assigned to any element inside as they would
/// in all other cases.
pub(crate) fn empty_parenthesized_with_dangling_comments(
opening: StaticText,
comments: &[SourceComment],
closing: StaticText,
) -> EmptyWithDanglingComments {
EmptyWithDanglingComments {
opening,
comments,
closing,
}
}
pub(crate) struct EmptyWithDanglingComments<'a> {
opening: StaticText,
comments: &'a [SourceComment],
closing: StaticText,
}
impl<'ast> Format<PyFormatContext<'ast>> for EmptyWithDanglingComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
let end_of_line_split = self
.comments
.partition_point(|comment| comment.line_position().is_end_of_line());
debug_assert!(self.comments[end_of_line_split..]
.iter()
.all(|comment| comment.line_position().is_own_line()));
write!(
f,
[group(&format_args![
self.opening,
// end-of-line comments
trailing_comments(&self.comments[..end_of_line_split]),
// Avoid unstable formatting with
// ```python
// x = () - (#
// )
// ```
// Without this the comment would go after the empty tuple first, but still expand
// the bin op. In the second formatting pass they are trailing bin op comments
// so the bin op collapse. Suboptimally we keep parentheses around the bin op in
// either case.
(!self.comments[..end_of_line_split].is_empty()).then_some(hard_line_break()),
// own line comments, which need to be indented
soft_block_indent(&dangling_comments(&self.comments[end_of_line_split..])),
self.closing
])]
)
}
}

View File

@@ -67,7 +67,10 @@ pub(super) fn place_comment<'a>(
handle_module_level_own_line_comment_before_class_or_function_comment(comment, locator)
}
AnyNodeRef::WithItem(_) => handle_with_item_comment(comment, locator),
AnyNodeRef::StmtFunctionDef(_) => handle_leading_function_with_decorators_comment(comment),
AnyNodeRef::StmtFunctionDef(function_def) => {
handle_leading_function_with_decorators_comment(comment)
.or_else(|comment| handle_leading_returns_comment(comment, function_def))
}
AnyNodeRef::StmtClassDef(class_def) => {
handle_leading_class_with_decorators_comment(comment, class_def)
}
@@ -783,6 +786,40 @@ fn handle_leading_function_with_decorators_comment(comment: DecoratedComment) ->
}
}
/// Handles end-of-line comments between function parameters and the return type annotation,
/// attaching them as dangling comments to the function instead of making them trailing
/// parameter comments.
///
/// ```python
/// def double(a: int) -> ( # Hello
/// int
/// ):
/// return 2*a
/// ```
fn handle_leading_returns_comment<'a>(
comment: DecoratedComment<'a>,
function_def: &'a ast::StmtFunctionDef,
) -> CommentPlacement<'a> {
let parameters = function_def.parameters.as_ref();
let Some(returns) = function_def.returns.as_deref() else {
return CommentPlacement::Default(comment);
};
let is_preceding_parameters = comment
.preceding_node()
.is_some_and(|node| node == parameters.into());
let is_following_returns = comment
.following_node()
.is_some_and(|node| node == returns.into());
if comment.line_position().is_end_of_line() && is_preceding_parameters && is_following_returns {
CommentPlacement::dangling(comment.enclosing_node(), comment)
} else {
CommentPlacement::Default(comment)
}
}
/// Handle comments between decorators and the decorated node.
///
/// For example, given:

View File

@@ -1,10 +1,6 @@
use std::iter::Peekable;
use ruff_python_ast::{
Alias, Arguments, Comprehension, Decorator, ElifElseClause, ExceptHandler, Expr, Keyword,
MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt, TypeParam,
TypeParams, WithItem,
};
use ruff_python_ast::{Mod, Ranged, Stmt};
use ruff_text_size::{TextRange, TextSize};
use ruff_formatter::{SourceCode, SourceCodeSlice};
@@ -48,14 +44,22 @@ impl<'a> CommentsVisitor<'a> {
self.finish()
}
fn start_node<N>(&mut self, node: N) -> TraversalSignal
where
N: Into<AnyNodeRef<'a>>,
{
self.start_node_impl(node.into())
// Try to skip the subtree if
// * there are no comments
// * if the next comment comes after this node (meaning, this nodes subtree contains no comments)
fn can_skip(&mut self, node_end: TextSize) -> bool {
self.comment_ranges
.peek()
.map_or(true, |next_comment| next_comment.start() >= node_end)
}
fn start_node_impl(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
fn finish(self) -> CommentsMap<'a> {
self.builder.finish()
}
}
impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> {
fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal {
let node_range = node.range();
let enclosing_node = self.parents.last().copied().unwrap_or(node);
@@ -95,23 +99,7 @@ impl<'a> CommentsVisitor<'a> {
}
}
// Try to skip the subtree if
// * there are no comments
// * if the next comment comes after this node (meaning, this nodes subtree contains no comments)
fn can_skip(&mut self, node_end: TextSize) -> bool {
self.comment_ranges
.peek()
.map_or(true, |next_comment| next_comment.start() >= node_end)
}
fn finish_node<N>(&mut self, node: N)
where
N: Into<AnyNodeRef<'a>>,
{
self.finish_node_impl(node.into());
}
fn finish_node_impl(&mut self, node: AnyNodeRef<'a>) {
fn leave_node(&mut self, node: AnyNodeRef<'ast>) {
// We are leaving this node, pop it from the parent stack.
self.parents.pop();
@@ -146,19 +134,6 @@ impl<'a> CommentsVisitor<'a> {
self.preceding_node = Some(node);
}
fn finish(self) -> CommentsMap<'a> {
self.builder.finish()
}
}
impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> {
fn visit_mod(&mut self, module: &'ast Mod) {
if self.start_node(module).is_traverse() {
walk_module(self, module);
}
self.finish_node(module);
}
fn visit_body(&mut self, body: &'ast [Stmt]) {
match body {
[] => {
@@ -178,140 +153,6 @@ impl<'ast> PreorderVisitor<'ast> for CommentsVisitor<'ast> {
}
}
}
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
if self.start_node(stmt).is_traverse() {
walk_stmt(self, stmt);
}
self.finish_node(stmt);
}
fn visit_annotation(&mut self, expr: &'ast Expr) {
if self.start_node(expr).is_traverse() {
walk_expr(self, expr);
}
self.finish_node(expr);
}
fn visit_decorator(&mut self, decorator: &'ast Decorator) {
if self.start_node(decorator).is_traverse() {
walk_decorator(self, decorator);
}
self.finish_node(decorator);
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if self.start_node(expr).is_traverse() {
walk_expr(self, expr);
}
self.finish_node(expr);
}
fn visit_comprehension(&mut self, comprehension: &'ast Comprehension) {
if self.start_node(comprehension).is_traverse() {
walk_comprehension(self, comprehension);
}
self.finish_node(comprehension);
}
fn visit_except_handler(&mut self, except_handler: &'ast ExceptHandler) {
if self.start_node(except_handler).is_traverse() {
walk_except_handler(self, except_handler);
}
self.finish_node(except_handler);
}
fn visit_format_spec(&mut self, format_spec: &'ast Expr) {
if self.start_node(format_spec).is_traverse() {
walk_expr(self, format_spec);
}
self.finish_node(format_spec);
}
fn visit_arguments(&mut self, arguments: &'ast Arguments) {
if self.start_node(arguments).is_traverse() {
walk_arguments(self, arguments);
}
self.finish_node(arguments);
}
fn visit_parameters(&mut self, parameters: &'ast Parameters) {
if self.start_node(parameters).is_traverse() {
walk_parameters(self, parameters);
}
self.finish_node(parameters);
}
fn visit_parameter(&mut self, arg: &'ast Parameter) {
if self.start_node(arg).is_traverse() {
walk_parameter(self, arg);
}
self.finish_node(arg);
}
fn visit_parameter_with_default(&mut self, parameter_with_default: &'ast ParameterWithDefault) {
if self.start_node(parameter_with_default).is_traverse() {
walk_parameter_with_default(self, parameter_with_default);
}
self.finish_node(parameter_with_default);
}
fn visit_keyword(&mut self, keyword: &'ast Keyword) {
if self.start_node(keyword).is_traverse() {
walk_keyword(self, keyword);
}
self.finish_node(keyword);
}
fn visit_alias(&mut self, alias: &'ast Alias) {
if self.start_node(alias).is_traverse() {
walk_alias(self, alias);
}
self.finish_node(alias);
}
fn visit_with_item(&mut self, with_item: &'ast WithItem) {
if self.start_node(with_item).is_traverse() {
walk_with_item(self, with_item);
}
self.finish_node(with_item);
}
fn visit_match_case(&mut self, match_case: &'ast MatchCase) {
if self.start_node(match_case).is_traverse() {
walk_match_case(self, match_case);
}
self.finish_node(match_case);
}
fn visit_pattern(&mut self, pattern: &'ast Pattern) {
if self.start_node(pattern).is_traverse() {
walk_pattern(self, pattern);
}
self.finish_node(pattern);
}
fn visit_elif_else_clause(&mut self, elif_else_clause: &'ast ElifElseClause) {
if self.start_node(elif_else_clause).is_traverse() {
walk_elif_else_clause(self, elif_else_clause);
}
self.finish_node(elif_else_clause);
}
fn visit_type_params(&mut self, type_params: &'ast TypeParams) {
if self.start_node(type_params).is_traverse() {
walk_type_params(self, type_params);
}
self.finish_node(type_params);
}
fn visit_type_param(&mut self, type_param: &'ast TypeParam) {
if self.start_node(type_param).is_traverse() {
walk_type_param(self, type_param);
}
self.finish_node(type_param);
}
}
fn text_position(comment_range: TextRange, source_code: SourceCode) -> CommentLinePosition {
@@ -663,18 +504,6 @@ impl<'a> CommentPlacement<'a> {
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum TraversalSignal {
Traverse,
Skip,
}
impl TraversalSignal {
const fn is_traverse(self) -> bool {
matches!(self, TraversalSignal::Traverse)
}
}
#[derive(Clone, Debug, Default)]
struct CommentsBuilder<'a> {
comments: CommentsMap<'a>,

View File

@@ -4,9 +4,10 @@ use ruff_python_ast::Ranged;
use ruff_python_ast::{Expr, ExprDict};
use ruff_text_size::TextRange;
use crate::builders::empty_parenthesized_with_dangling_comments;
use crate::comments::leading_comments;
use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
use crate::FormatNodeRule;
@@ -69,8 +70,7 @@ impl FormatNodeRule<ExprDict> for FormatExprDict {
let dangling = comments.dangling_comments(item);
if values.is_empty() {
return empty_parenthesized_with_dangling_comments(text("{"), dangling, text("}"))
.fmt(f);
return empty_parenthesized("{", dangling, "}").fmt(f);
}
let format_pairs = format_with(|f| {

View File

@@ -0,0 +1,12 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::ExprIpyEscapeCommand;
#[derive(Default)]
pub struct FormatExprIpyEscapeCommand;
impl FormatNodeRule<ExprIpyEscapeCommand> for FormatExprIpyEscapeCommand {
fn fmt_fields(&self, item: &ExprIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item)])
}
}

View File

@@ -1,12 +0,0 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::ExprLineMagic;
#[derive(Default)]
pub struct FormatExprLineMagic;
impl FormatNodeRule<ExprLineMagic> for FormatExprLineMagic {
fn fmt_fields(&self, item: &ExprLineMagic, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item)])
}
}

View File

@@ -1,9 +1,10 @@
use ruff_formatter::prelude::{format_with, text};
use ruff_formatter::prelude::format_with;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{ExprList, Ranged};
use crate::builders::empty_parenthesized_with_dangling_comments;
use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses};
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
use crate::FormatNodeRule;
@@ -22,8 +23,7 @@ impl FormatNodeRule<ExprList> for FormatExprList {
let dangling = comments.dangling_comments(item);
if elts.is_empty() {
return empty_parenthesized_with_dangling_comments(text("["), dangling, text("]"))
.fmt(f);
return empty_parenthesized("[", dangling, "]").fmt(f);
}
let items = format_with(|f| {

View File

@@ -4,8 +4,10 @@ use ruff_python_ast::ExprTuple;
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::TextRange;
use crate::builders::{empty_parenthesized_with_dangling_comments, parenthesize_if_expands};
use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses};
use crate::builders::parenthesize_if_expands;
use crate::expression::parentheses::{
empty_parenthesized, parenthesized, NeedsParentheses, OptionalParentheses,
};
use crate::prelude::*;
#[derive(Eq, PartialEq, Debug, Default)]
@@ -117,8 +119,7 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
// In all other cases comments get assigned to a list element
match elts.as_slice() {
[] => {
return empty_parenthesized_with_dangling_comments(text("("), dangling, text(")"))
.fmt(f);
return empty_parenthesized("(", dangling, ")").fmt(f);
}
[single] => match self.parentheses {
TupleParentheses::Preserve

View File

@@ -31,8 +31,8 @@ pub(crate) mod expr_f_string;
pub(crate) mod expr_formatted_value;
pub(crate) mod expr_generator_exp;
pub(crate) mod expr_if_exp;
pub(crate) mod expr_ipy_escape_command;
pub(crate) mod expr_lambda;
pub(crate) mod expr_line_magic;
pub(crate) mod expr_list;
pub(crate) mod expr_list_comp;
pub(crate) mod expr_name;
@@ -102,7 +102,7 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
Expr::List(expr) => expr.format().fmt(f),
Expr::Tuple(expr) => expr.format().fmt(f),
Expr::Slice(expr) => expr.format().fmt(f),
Expr::LineMagic(_) => todo!(),
Expr::IpyEscapeCommand(_) => todo!(),
});
let parenthesize = match parentheses {
@@ -240,7 +240,7 @@ impl NeedsParentheses for Expr {
Expr::List(expr) => expr.needs_parentheses(parent, context),
Expr::Tuple(expr) => expr.needs_parentheses(parent, context),
Expr::Slice(expr) => expr.needs_parentheses(parent, context),
Expr::LineMagic(_) => todo!(),
Expr::IpyEscapeCommand(_) => todo!(),
}
}
}
@@ -434,7 +434,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
| Expr::Starred(_)
| Expr::Name(_)
| Expr::Slice(_) => {}
Expr::LineMagic(_) => todo!(),
Expr::IpyEscapeCommand(_) => todo!(),
};
walk_expr(self, expr);

View File

@@ -4,7 +4,9 @@ use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::Ranged;
use ruff_python_trivia::{first_non_trivia_token, SimpleToken, SimpleTokenKind, SimpleTokenizer};
use crate::comments::{dangling_open_parenthesis_comments, SourceComment};
use crate::comments::{
dangling_comments, dangling_open_parenthesis_comments, trailing_comments, SourceComment,
};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::prelude::*;
@@ -307,6 +309,67 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatInParenthesesOnlyGroup<'_, 'a
}
}
/// Format comments inside empty parentheses, brackets or curly braces.
///
/// Empty `()`, `[]` and `{}` are special because there can be dangling comments, and they can be in
/// two positions:
/// ```python
/// x = [ # end-of-line
/// # own line
/// ]
/// ```
/// These comments are dangling because they can't be assigned to any element inside as they would
/// in all other cases.
pub(crate) fn empty_parenthesized<'content>(
left: &'static str,
comments: &'content [SourceComment],
right: &'static str,
) -> FormatEmptyParenthesized<'content> {
FormatEmptyParenthesized {
left,
comments,
right,
}
}
pub(crate) struct FormatEmptyParenthesized<'content> {
left: &'static str,
comments: &'content [SourceComment],
right: &'static str,
}
impl Format<PyFormatContext<'_>> for FormatEmptyParenthesized<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
let end_of_line_split = self
.comments
.partition_point(|comment| comment.line_position().is_end_of_line());
debug_assert!(self.comments[end_of_line_split..]
.iter()
.all(|comment| comment.line_position().is_own_line()));
write!(
f,
[group(&format_args![
text(self.left),
// end-of-line comments
trailing_comments(&self.comments[..end_of_line_split]),
// Avoid unstable formatting with
// ```python
// x = () - (#
// )
// ```
// Without this the comment would go after the empty tuple first, but still expand
// the bin op. In the second formatting pass they are trailing bin op comments
// so the bin op collapse. Suboptimally we keep parentheses around the bin op in
// either case.
(!self.comments[..end_of_line_split].is_empty()).then_some(hard_line_break()),
// own line comments, which need to be indented
soft_block_indent(&dangling_comments(&self.comments[end_of_line_split..])),
text(self.right)
])]
)
}
}
#[cfg(test)]
mod tests {
use ruff_python_ast::node::AnyNodeRef;

View File

@@ -930,38 +930,38 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::StmtContinue {
}
}
impl FormatRule<ast::StmtLineMagic, PyFormatContext<'_>>
for crate::statement::stmt_line_magic::FormatStmtLineMagic
impl FormatRule<ast::StmtIpyEscapeCommand, PyFormatContext<'_>>
for crate::statement::stmt_ipy_escape_command::FormatStmtIpyEscapeCommand
{
#[inline]
fn fmt(&self, node: &ast::StmtLineMagic, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::StmtLineMagic>::fmt(self, node, f)
fn fmt(&self, node: &ast::StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::StmtIpyEscapeCommand>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::StmtLineMagic {
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::StmtIpyEscapeCommand {
type Format<'a> = FormatRefWithRule<
'a,
ast::StmtLineMagic,
crate::statement::stmt_line_magic::FormatStmtLineMagic,
ast::StmtIpyEscapeCommand,
crate::statement::stmt_ipy_escape_command::FormatStmtIpyEscapeCommand,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::statement::stmt_line_magic::FormatStmtLineMagic::default(),
crate::statement::stmt_ipy_escape_command::FormatStmtIpyEscapeCommand::default(),
)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::StmtLineMagic {
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::StmtIpyEscapeCommand {
type Format = FormatOwnedWithRule<
ast::StmtLineMagic,
crate::statement::stmt_line_magic::FormatStmtLineMagic,
ast::StmtIpyEscapeCommand,
crate::statement::stmt_ipy_escape_command::FormatStmtIpyEscapeCommand,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::statement::stmt_line_magic::FormatStmtLineMagic::default(),
crate::statement::stmt_ipy_escape_command::FormatStmtIpyEscapeCommand::default(),
)
}
}
@@ -1930,38 +1930,38 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprSlice {
}
}
impl FormatRule<ast::ExprLineMagic, PyFormatContext<'_>>
for crate::expression::expr_line_magic::FormatExprLineMagic
impl FormatRule<ast::ExprIpyEscapeCommand, PyFormatContext<'_>>
for crate::expression::expr_ipy_escape_command::FormatExprIpyEscapeCommand
{
#[inline]
fn fmt(&self, node: &ast::ExprLineMagic, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::ExprLineMagic>::fmt(self, node, f)
fn fmt(&self, node: &ast::ExprIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
FormatNodeRule::<ast::ExprIpyEscapeCommand>::fmt(self, node, f)
}
}
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::ExprLineMagic {
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::ExprIpyEscapeCommand {
type Format<'a> = FormatRefWithRule<
'a,
ast::ExprLineMagic,
crate::expression::expr_line_magic::FormatExprLineMagic,
ast::ExprIpyEscapeCommand,
crate::expression::expr_ipy_escape_command::FormatExprIpyEscapeCommand,
PyFormatContext<'ast>,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::expression::expr_line_magic::FormatExprLineMagic::default(),
crate::expression::expr_ipy_escape_command::FormatExprIpyEscapeCommand::default(),
)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprLineMagic {
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::ExprIpyEscapeCommand {
type Format = FormatOwnedWithRule<
ast::ExprLineMagic,
crate::expression::expr_line_magic::FormatExprLineMagic,
ast::ExprIpyEscapeCommand,
crate::expression::expr_ipy_escape_command::FormatExprIpyEscapeCommand,
PyFormatContext<'ast>,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::expression::expr_line_magic::FormatExprLineMagic::default(),
crate::expression::expr_ipy_escape_command::FormatExprIpyEscapeCommand::default(),
)
}
}

View File

@@ -4,9 +4,8 @@ use ruff_python_ast::{Arguments, Expr, Ranged};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
use crate::builders::empty_parenthesized_with_dangling_comments;
use crate::expression::expr_generator_exp::GeneratorExpParentheses;
use crate::expression::parentheses::{parenthesized, Parentheses};
use crate::expression::parentheses::{empty_parenthesized, parenthesized, Parentheses};
use crate::prelude::*;
use crate::FormatNodeRule;
@@ -24,14 +23,8 @@ impl FormatNodeRule<Arguments> for FormatArguments {
// ```
if item.args.is_empty() && item.keywords.is_empty() {
let comments = f.context().comments().clone();
return write!(
f,
[empty_parenthesized_with_dangling_comments(
text("("),
comments.dangling_comments(item),
text(")"),
)]
);
let dangling = comments.dangling_comments(item);
return write!(f, [empty_parenthesized("(", dangling, ")")]);
}
let all_arguments = format_with(|f: &mut PyFormatter| {

View File

@@ -6,16 +6,16 @@ use ruff_python_ast::{Parameters, Ranged};
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{TextRange, TextSize};
use crate::builders::empty_parenthesized_with_dangling_comments;
use crate::comments::{
leading_comments, leading_node_comments, trailing_comments, CommentLinePosition, SourceComment,
dangling_open_parenthesis_comments, leading_comments, leading_node_comments, trailing_comments,
CommentLinePosition, SourceComment,
};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::parentheses::parenthesized;
use crate::expression::parentheses::empty_parenthesized;
use crate::prelude::*;
use crate::FormatNodeRule;
#[derive(Eq, PartialEq, Debug, Default)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ParametersParentheses {
/// By default, parameters will always preserve their surrounding parentheses.
#[default]
@@ -245,19 +245,19 @@ impl FormatNodeRule<Parameters> for FormatParameters {
write!(f, [group(&format_inner)])
} else if num_parameters == 0 {
// No parameters, format any dangling comments between `()`
write!(
f,
[empty_parenthesized_with_dangling_comments(
text("("),
dangling,
text(")"),
)]
)
write!(f, [empty_parenthesized("(", dangling, ")")])
} else {
// Intentionally avoid `parenthesized`, which groups the entire formatted contents.
// We want parameters to be grouped alongside return types, one level up, so we
// format them "inline" here.
write!(
f,
[parenthesized("(", &group(&format_inner), ")")
.with_dangling_comments(parenthesis_dangling)]
[
text("("),
dangling_open_parenthesis_comments(parenthesis_dangling),
soft_block_indent(&group(&format_inner)),
text(")")
]
)
}
}

View File

@@ -17,7 +17,7 @@ pub(crate) mod stmt_global;
pub(crate) mod stmt_if;
pub(crate) mod stmt_import;
pub(crate) mod stmt_import_from;
pub(crate) mod stmt_line_magic;
pub(crate) mod stmt_ipy_escape_command;
pub(crate) mod stmt_match;
pub(crate) mod stmt_nonlocal;
pub(crate) mod stmt_pass;
@@ -61,7 +61,7 @@ impl FormatRule<Stmt, PyFormatContext<'_>> for FormatStmt {
Stmt::Break(x) => x.format().fmt(f),
Stmt::Continue(x) => x.format().fmt(f),
Stmt::TypeAlias(x) => x.format().fmt(f),
Stmt::LineMagic(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}
}

View File

@@ -58,21 +58,28 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
write!(f, [type_params.format()])?;
}
write!(f, [item.parameters.format()])?;
let format_inner = format_with(|f: &mut PyFormatter| {
write!(f, [item.parameters.format()])?;
if let Some(return_annotation) = item.returns.as_ref() {
write!(f, [space(), text("->"), space()])?;
if return_annotation.is_tuple_expr() {
write!(
f,
[return_annotation.format().with_options(Parentheses::Never)]
)?;
} else {
write!(
f,
[optional_parentheses(
&return_annotation.format().with_options(Parentheses::Never),
)]
)?;
}
}
Ok(())
});
if let Some(return_annotation) = item.returns.as_ref() {
write!(
f,
[
space(),
text("->"),
space(),
optional_parentheses(
&return_annotation.format().with_options(Parentheses::Never)
)
]
)?;
}
write!(f, [group(&format_inner)])?;
write!(
f,

View File

@@ -0,0 +1,12 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::StmtIpyEscapeCommand;
#[derive(Default)]
pub struct FormatStmtIpyEscapeCommand;
impl FormatNodeRule<StmtIpyEscapeCommand> for FormatStmtIpyEscapeCommand {
fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item)])
}
}

View File

@@ -1,12 +0,0 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult};
use ruff_python_ast::StmtLineMagic;
#[derive(Default)]
pub struct FormatStmtLineMagic;
impl FormatNodeRule<StmtLineMagic> for FormatStmtLineMagic {
fn fmt_fields(&self, item: &StmtLineMagic, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item)])
}
}

View File

@@ -100,18 +100,20 @@ def foo() -> tuple[int, int, int,]:
```diff
--- Black
+++ Ruff
@@ -26,7 +26,9 @@
@@ -26,7 +26,11 @@
return 2 * a
-def double(a: int) -> int: # Hello
+def double(a: int) -> (
+def double(
+ a: int
+) -> (
+ int # Hello
+):
return 2 * a
@@ -54,7 +56,9 @@
@@ -54,7 +58,9 @@
a: int,
b: int,
c: int,
@@ -155,7 +157,9 @@ def double(a: int) -> int: # Hello
return 2 * a
def double(a: int) -> (
def double(
a: int
) -> (
int # Hello
):
return 2 * a

View File

@@ -377,6 +377,70 @@ def f( # first
# third
):
...
# Handle comments on empty tuple return types.
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
# comment
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
1
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
1, 2
): ...
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> ( # type: ignore[override]
(1, 2)
): ...
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
self, m: Match[str], data: str
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
...
def double(a: int # Hello
) -> (int):
return 2 * a
def double(a: int) -> ( # Hello
int
):
return 2*a
def double(a: int) -> ( # Hello
):
return 2*a
# Breaking over parameters and return types. (Black adds a trailing comma when the
# function arguments break here with a single argument; we do not.)
def f(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, a) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> a:
...
def f(a) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> a:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa) -> a:
...
```
## Output
@@ -905,6 +969,141 @@ def f( # first
/, # second
):
...
# Handle comments on empty tuple return types.
def zrevrangebylex(
self,
name: _Key,
max: _Value,
min: _Value,
start: int | None = None,
num: int | None = None,
) -> ( # type: ignore[override]
):
...
def zrevrangebylex(
self,
name: _Key,
max: _Value,
min: _Value,
start: int | None = None,
num: int | None = None,
) -> ( # type: ignore[override]
# comment
):
...
def zrevrangebylex(
self,
name: _Key,
max: _Value,
min: _Value,
start: int | None = None,
num: int | None = None,
) -> 1: # type: ignore[override]
...
def zrevrangebylex(
self,
name: _Key,
max: _Value,
min: _Value,
start: int | None = None,
num: int | None = None,
) -> ( # type: ignore[override]
1,
2,
):
...
def zrevrangebylex(
self,
name: _Key,
max: _Value,
min: _Value,
start: int | None = None,
num: int | None = None,
) -> (1, 2): # type: ignore[override]
...
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
self, m: Match[str], data: str
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
...
def double(
a: int, # Hello
) -> int:
return 2 * a
def double(a: int) -> int: # Hello
return 2 * a
def double(
a: int
) -> ( # Hello
):
return 2 * a
# Breaking over parameters and return types. (Black adds a trailing comma when the
# function arguments break here with a single argument; we do not.)
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, a
) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
) -> a:
...
def f(
a
) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]() -> (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
):
...
def f[
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
]() -> a:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
) -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
...
def f[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa](
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
) -> a:
...
```

View File

@@ -34,7 +34,7 @@ use std::{char, cmp::Ordering, str::FromStr};
use num_bigint::BigInt;
use num_traits::{Num, Zero};
use ruff_python_ast::MagicKind;
use ruff_python_ast::IpyEscapeKind;
use ruff_text_size::{TextLen, TextRange, TextSize};
use unic_emoji_char::is_emoji_presentation;
use unic_ucd_ident::{is_xid_continue, is_xid_start};
@@ -398,8 +398,8 @@ impl<'source> Lexer<'source> {
Tok::Comment(self.token_text().to_string())
}
/// Lex a single magic command.
fn lex_magic_command(&mut self, kind: MagicKind) -> Tok {
/// Lex a single IPython escape command.
fn lex_ipython_escape_command(&mut self, escape_kind: IpyEscapeKind) -> Tok {
let mut value = String::new();
loop {
@@ -457,7 +457,7 @@ impl<'source> Lexer<'source> {
// Now, the whitespace and empty value check also makes sure that an empty
// command (e.g. `%?` or `? ??`, no value after/between the escape tokens)
// is not recognized as a help end escape command. So, `%?` and `? ??` are
// `MagicKind::Magic` and `MagicKind::Help` because of the initial `%` and `??`
// `IpyEscapeKind::Magic` and `IpyEscapeKind::Help` because of the initial `%` and `??`
// tokens.
if question_count > 2
|| value.chars().last().map_or(true, is_python_whitespace)
@@ -471,31 +471,34 @@ impl<'source> Lexer<'source> {
continue;
}
if kind.is_help() {
if escape_kind.is_help() {
// If we've recognize this as a help end escape command, then
// any question mark token / whitespaces at the start are not
// considered as part of the value.
//
// For example, `??foo?` is recognized as `MagicKind::Help` and
// For example, `??foo?` is recognized as `IpyEscapeKind::Help` and
// `value` is `foo` instead of `??foo`.
value = value.trim_start_matches([' ', '?']).to_string();
} else if kind.is_magic() {
} else if escape_kind.is_magic() {
// Between `%` and `?` (at the end), the `?` takes priority
// over the `%` so `%foo?` is recognized as `MagicKind::Help`
// over the `%` so `%foo?` is recognized as `IpyEscapeKind::Help`
// and `value` is `%foo` instead of `foo`. So, we need to
// insert the magic escape token at the start.
value.insert_str(0, kind.as_str());
value.insert_str(0, escape_kind.as_str());
}
let kind = match question_count {
1 => MagicKind::Help,
2 => MagicKind::Help2,
1 => IpyEscapeKind::Help,
2 => IpyEscapeKind::Help2,
_ => unreachable!("`question_count` is always 1 or 2"),
};
return Tok::MagicCommand { kind, value };
return Tok::IpyEscapeCommand { kind, value };
}
'\n' | '\r' | EOF_CHAR => {
return Tok::MagicCommand { kind, value };
return Tok::IpyEscapeCommand {
kind: escape_kind,
value,
};
}
c => {
self.cursor.bump();
@@ -763,23 +766,26 @@ impl<'source> Lexer<'source> {
&& self.state.is_after_equal()
&& self.nesting == 0 =>
{
// SAFETY: Safe because `c` has been matched against one of the possible magic command prefix
self.lex_magic_command(MagicKind::try_from(c).unwrap())
// SAFETY: Safe because `c` has been matched against one of the possible escape command token
self.lex_ipython_escape_command(IpyEscapeKind::try_from(c).unwrap())
}
c @ ('%' | '!' | '?' | '/' | ';' | ',')
if self.mode == Mode::Jupyter && self.state.is_new_logical_line() =>
{
let kind = if let Ok(kind) = MagicKind::try_from([c, self.cursor.first()]) {
let kind = if let Ok(kind) = IpyEscapeKind::try_from([c, self.cursor.first()]) {
self.cursor.bump();
kind
} else {
// SAFETY: Safe because `c` has been matched against one of the possible magic command prefix
MagicKind::try_from(c).unwrap()
// SAFETY: Safe because `c` has been matched against one of the possible escape command token
IpyEscapeKind::try_from(c).unwrap()
};
self.lex_magic_command(kind)
self.lex_ipython_escape_command(kind)
}
'?' if self.mode == Mode::Jupyter => Tok::Question,
'/' => {
if self.cursor.eat_char('=') {
Tok::SlashEqual
@@ -1205,7 +1211,7 @@ const fn is_python_whitespace(c: char) -> bool {
#[cfg(test)]
mod tests {
use num_bigint::BigInt;
use ruff_python_ast::MagicKind;
use ruff_python_ast::IpyEscapeKind;
use super::*;
@@ -1239,15 +1245,15 @@ mod tests {
}
}
fn assert_jupyter_magic_line_continuation_with_eol(eol: &str) {
fn assert_ipython_escape_command_line_continuation_with_eol(eol: &str) {
let source = format!("%matplotlib \\{eol} --inline");
let tokens = lex_jupyter_source(&source);
assert_eq!(
tokens,
vec![
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "matplotlib --inline".to_string(),
kind: MagicKind::Magic
kind: IpyEscapeKind::Magic
},
Tok::Newline
]
@@ -1255,29 +1261,29 @@ mod tests {
}
#[test]
fn test_jupyter_magic_line_continuation_unix_eol() {
assert_jupyter_magic_line_continuation_with_eol(UNIX_EOL);
fn test_ipython_escape_command_line_continuation_unix_eol() {
assert_ipython_escape_command_line_continuation_with_eol(UNIX_EOL);
}
#[test]
fn test_jupyter_magic_line_continuation_mac_eol() {
assert_jupyter_magic_line_continuation_with_eol(MAC_EOL);
fn test_ipython_escape_command_line_continuation_mac_eol() {
assert_ipython_escape_command_line_continuation_with_eol(MAC_EOL);
}
#[test]
fn test_jupyter_magic_line_continuation_windows_eol() {
assert_jupyter_magic_line_continuation_with_eol(WINDOWS_EOL);
fn test_ipython_escape_command_line_continuation_windows_eol() {
assert_ipython_escape_command_line_continuation_with_eol(WINDOWS_EOL);
}
fn assert_jupyter_magic_line_continuation_with_eol_and_eof(eol: &str) {
fn assert_ipython_escape_command_line_continuation_with_eol_and_eof(eol: &str) {
let source = format!("%matplotlib \\{eol}");
let tokens = lex_jupyter_source(&source);
assert_eq!(
tokens,
vec![
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "matplotlib ".to_string(),
kind: MagicKind::Magic
kind: IpyEscapeKind::Magic
},
Tok::Newline
]
@@ -1285,70 +1291,70 @@ mod tests {
}
#[test]
fn test_jupyter_magic_line_continuation_unix_eol_and_eof() {
assert_jupyter_magic_line_continuation_with_eol_and_eof(UNIX_EOL);
fn test_ipython_escape_command_line_continuation_unix_eol_and_eof() {
assert_ipython_escape_command_line_continuation_with_eol_and_eof(UNIX_EOL);
}
#[test]
fn test_jupyter_magic_line_continuation_mac_eol_and_eof() {
assert_jupyter_magic_line_continuation_with_eol_and_eof(MAC_EOL);
fn test_ipython_escape_command_line_continuation_mac_eol_and_eof() {
assert_ipython_escape_command_line_continuation_with_eol_and_eof(MAC_EOL);
}
#[test]
fn test_jupyter_magic_line_continuation_windows_eol_and_eof() {
assert_jupyter_magic_line_continuation_with_eol_and_eof(WINDOWS_EOL);
fn test_ipython_escape_command_line_continuation_windows_eol_and_eof() {
assert_ipython_escape_command_line_continuation_with_eol_and_eof(WINDOWS_EOL);
}
#[test]
fn test_empty_jupyter_magic() {
fn test_empty_ipython_escape_command() {
let source = "%\n%%\n!\n!!\n?\n??\n/\n,\n;";
let tokens = lex_jupyter_source(source);
assert_eq!(
tokens,
vec![
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Magic2,
kind: IpyEscapeKind::Magic2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Shell,
kind: IpyEscapeKind::Shell,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::ShCap,
kind: IpyEscapeKind::ShCap,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Paren,
kind: IpyEscapeKind::Paren,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Quote,
kind: IpyEscapeKind::Quote,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: String::new(),
kind: MagicKind::Quote2,
kind: IpyEscapeKind::Quote2,
},
Tok::Newline,
]
@@ -1356,7 +1362,7 @@ mod tests {
}
#[test]
fn test_jupyter_magic() {
fn test_ipython_escape_command() {
let source = r"
?foo
??foo
@@ -1377,59 +1383,59 @@ mod tests {
assert_eq!(
tokens,
vec![
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "timeit a = b".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "timeit a % 3".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "matplotlib --inline".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "pwd && ls -a | sed 's/^/\\\\ /'".to_string(),
kind: MagicKind::Shell,
kind: IpyEscapeKind::Shell,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "cd /Users/foo/Library/Application\\ Support/".to_string(),
kind: MagicKind::ShCap,
kind: IpyEscapeKind::ShCap,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo 1 2".to_string(),
kind: MagicKind::Paren,
kind: IpyEscapeKind::Paren,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo 1 2".to_string(),
kind: MagicKind::Quote,
kind: IpyEscapeKind::Quote,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo 1 2".to_string(),
kind: MagicKind::Quote2,
kind: IpyEscapeKind::Quote2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "ls".to_string(),
kind: MagicKind::Shell,
kind: IpyEscapeKind::Shell,
},
Tok::Newline,
]
@@ -1437,7 +1443,7 @@ mod tests {
}
#[test]
fn test_jupyter_magic_help_end() {
fn test_ipython_help_end_escape_command() {
let source = r"
?foo?
?? foo?
@@ -1462,84 +1468,84 @@ mod tests {
assert_eq!(
tokens,
[
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: " foo ?".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo???".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "?foo???".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: " ?".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "??".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "%foo".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "%foo".to_string(),
kind: MagicKind::Help2,
kind: IpyEscapeKind::Help2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "foo???".to_string(),
kind: MagicKind::Magic2,
kind: IpyEscapeKind::Magic2,
},
Tok::Newline,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "pwd".to_string(),
kind: MagicKind::Help,
kind: IpyEscapeKind::Help,
},
Tok::Newline,
]
@@ -1547,7 +1553,7 @@ mod tests {
}
#[test]
fn test_jupyter_magic_indentation() {
fn test_ipython_escape_command_indentation() {
let source = r"
if True:
%matplotlib \
@@ -1562,9 +1568,9 @@ if True:
Tok::Colon,
Tok::Newline,
Tok::Indent,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "matplotlib --inline".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::Dedent,
@@ -1573,7 +1579,7 @@ if True:
}
#[test]
fn test_jupyter_magic_assignment() {
fn test_ipython_escape_command_assignment() {
let source = r"
pwd = !pwd
foo = %timeit a = b
@@ -1589,54 +1595,54 @@ baz = %matplotlib \
name: "pwd".to_string()
},
Tok::Equal,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "pwd".to_string(),
kind: MagicKind::Shell,
kind: IpyEscapeKind::Shell,
},
Tok::Newline,
Tok::Name {
name: "foo".to_string()
},
Tok::Equal,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "timeit a = b".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::Name {
name: "bar".to_string()
},
Tok::Equal,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "timeit a % 3".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
Tok::Name {
name: "baz".to_string()
},
Tok::Equal,
Tok::MagicCommand {
Tok::IpyEscapeCommand {
value: "matplotlib inline".to_string(),
kind: MagicKind::Magic,
kind: IpyEscapeKind::Magic,
},
Tok::Newline,
]
);
}
fn assert_no_jupyter_magic(tokens: &[Tok]) {
fn assert_no_ipython_escape_command(tokens: &[Tok]) {
for tok in tokens {
if let Tok::MagicCommand { .. } = tok {
panic!("Unexpected magic command token: {tok:?}")
if let Tok::IpyEscapeCommand { .. } = tok {
panic!("Unexpected escape command token: {tok:?}")
}
}
}
#[test]
fn test_jupyter_magic_not_an_assignment() {
fn test_ipython_escape_command_not_an_assignment() {
let source = r"
# Other magic kinds are not valid here (can't test `foo = ?str` because '?' is not a valid token)
# Other escape kinds are not valid here (can't test `foo = ?str` because '?' is not a valid token)
foo = /func
foo = ;func
foo = ,func
@@ -1647,7 +1653,7 @@ def f(arg=%timeit a = b):
pass"
.trim();
let tokens = lex_jupyter_source(source);
assert_no_jupyter_magic(&tokens);
assert_no_ipython_escape_command(&tokens);
}
#[test]

View File

@@ -117,7 +117,7 @@ pub fn parse_expression_starts_at(
///
/// This function is the most general function to parse Python code. Based on the [`Mode`] supplied,
/// it can be used to parse a single expression, a full Python program, an interactive expression
/// or a Python program containing Jupyter magics.
/// or a Python program containing IPython escape commands.
///
/// # Example
///
@@ -146,7 +146,7 @@ pub fn parse_expression_starts_at(
/// assert!(program.is_ok());
/// ```
///
/// Additionally, we can parse a Python program containing Jupyter magics:
/// Additionally, we can parse a Python program containing IPython escapes:
///
/// ```
/// use ruff_python_parser::{Mode, parse};
@@ -1122,7 +1122,7 @@ class Abcd:
}
#[test]
fn test_jupyter_magic() {
fn test_ipython_escape_commands() {
let parse_ast = parse(
r#"
# Normal Python code
@@ -1169,7 +1169,7 @@ def foo():
;foo 1 2
,foo 1 2
# Indented magic
# Indented escape commands
for a in range(5):
!ls
@@ -1180,6 +1180,15 @@ foo = %foo \
% foo
foo = %foo # comment
# Help end line magics
foo?
foo.bar??
foo.bar.baz?
foo[0]??
foo[0][1]?
foo.bar[0].baz[1]??
foo.bar[0].baz[2].egg??
"#
.trim(),
Mode::Jupyter,
@@ -1190,7 +1199,7 @@ foo = %foo # comment
}
#[test]
fn test_jupyter_magic_parse_error() {
fn test_ipython_escape_command_parse_error() {
let source = r#"
a = 1
%timeit a == 1
@@ -1200,7 +1209,7 @@ a = 1
let parse_err = parse_tokens(lxr, Mode::Module, "<test>").unwrap_err();
assert_eq!(
parse_err.to_string(),
"line magics are only allowed in Jupyter mode at byte offset 6".to_string()
"IPython escape commands are only allowed in Jupyter mode at byte offset 6".to_string()
);
}
}

View File

@@ -5,7 +5,7 @@
use num_bigint::BigInt;
use ruff_text_size::TextSize;
use ruff_python_ast::{self as ast, Ranged, MagicKind};
use ruff_python_ast::{self as ast, Ranged, IpyEscapeKind};
use crate::{
Mode,
lexer::{LexicalError, LexicalErrorType},
@@ -14,6 +14,7 @@ use crate::{
string::parse_strings,
token::{self, StringKind},
};
use lalrpop_util::ParseError;
grammar(mode: Mode);
@@ -88,7 +89,8 @@ SmallStatement: ast::Stmt = {
NonlocalStatement,
AssertStatement,
TypeAliasStatement,
LineMagicStatement,
IpyEscapeCommandStatement,
IpyHelpEndEscapeCommandStatement,
};
PassStatement: ast::Stmt = {
@@ -153,7 +155,7 @@ ExpressionStatement: ast::Stmt = {
AssignSuffix: ast::Expr = {
"=" <e:TestListOrYieldExpr> => e,
"=" <e:LineMagicExpr> => e
"=" <e:IpyEscapeCommandExpr> => e
};
TestListOrYieldExpr: ast::Expr = {
@@ -321,51 +323,123 @@ AssertStatement: ast::Stmt = {
},
};
LineMagicStatement: ast::Stmt = {
<location:@L> <m:line_magic> <end_location:@R> =>? {
IpyEscapeCommandStatement: ast::Stmt = {
<location:@L> <c:ipy_escape_command> <end_location:@R> =>? {
if mode == Mode::Jupyter {
Ok(ast::Stmt::LineMagic(
ast::StmtLineMagic {
kind: m.0,
value: m.1,
Ok(ast::Stmt::IpyEscapeCommand(
ast::StmtIpyEscapeCommand {
kind: c.0,
value: c.1,
range: (location..end_location).into()
}
))
} else {
Err(LexicalError {
error: LexicalErrorType::OtherError("line magics are only allowed in Jupyter mode".to_string()),
error: LexicalErrorType::OtherError("IPython escape commands are only allowed in Jupyter mode".to_string()),
location,
})?
}
}
}
LineMagicExpr: ast::Expr = {
<location:@L> <m:line_magic> <end_location:@R> =>? {
IpyEscapeCommandExpr: ast::Expr = {
<location:@L> <c:ipy_escape_command> <end_location:@R> =>? {
if mode == Mode::Jupyter {
// This should never occur as the lexer won't allow it.
if !matches!(m.0, MagicKind::Magic | MagicKind::Shell) {
if !matches!(c.0, IpyEscapeKind::Magic | IpyEscapeKind::Shell) {
return Err(LexicalError {
error: LexicalErrorType::OtherError("expr line magics are only allowed for % and !".to_string()),
error: LexicalErrorType::OtherError("IPython escape command expr is only allowed for % and !".to_string()),
location,
})?;
}
Ok(ast::Expr::LineMagic(
ast::ExprLineMagic {
kind: m.0,
value: m.1,
Ok(ast::Expr::IpyEscapeCommand(
ast::ExprIpyEscapeCommand {
kind: c.0,
value: c.1,
range: (location..end_location).into()
}
))
} else {
Err(LexicalError {
error: LexicalErrorType::OtherError("line magics are only allowed in Jupyter mode".to_string()),
error: LexicalErrorType::OtherError("IPython escape commands are only allowed in Jupyter mode".to_string()),
location,
})?
}
}
}
IpyHelpEndEscapeCommandStatement: ast::Stmt = {
// We are permissive than the original implementation because we would allow whitespace
// between the expression and the suffix while the IPython implementation doesn't allow it.
// For example, `foo ?` would be valid in our case but invalid from IPython.
<location:@L> <e:Expression<"All">> <suffix:("?")+> <end_location:@R> =>? {
fn unparse_expr(expr: &ast::Expr, buffer: &mut String) -> Result<(), LexicalError> {
match expr {
ast::Expr::Name(ast::ExprName { id, .. }) => {
buffer.push_str(id.as_str());
},
ast::Expr::Subscript(ast::ExprSubscript { value, slice, range, .. }) => {
let ast::Expr::Constant(ast::ExprConstant { value: ast::Constant::Int(integer), .. }) = slice.as_ref() else {
return Err(LexicalError {
error: LexicalErrorType::OtherError("only integer constants are allowed in Subscript expressions in help end escape command".to_string()),
location: range.start(),
});
};
unparse_expr(value, buffer)?;
buffer.push('[');
buffer.push_str(&format!("{}", integer));
buffer.push(']');
},
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
unparse_expr(value, buffer)?;
buffer.push('.');
buffer.push_str(attr.as_str());
},
_ => {
return Err(LexicalError {
error: LexicalErrorType::OtherError("only Name, Subscript and Attribute expressions are allowed in help end escape command".to_string()),
location: expr.range().start(),
});
}
}
Ok(())
}
if mode != Mode::Jupyter {
return Err(ParseError::User {
error: LexicalError {
error: LexicalErrorType::OtherError("IPython escape commands are only allowed in Jupyter mode".to_string()),
location,
},
});
}
let kind = match suffix.len() {
1 => IpyEscapeKind::Help,
2 => IpyEscapeKind::Help2,
_ => {
return Err(ParseError::User {
error: LexicalError {
error: LexicalErrorType::OtherError("maximum of 2 `?` tokens are allowed in help end escape command".to_string()),
location,
},
});
}
};
let mut value = String::new();
unparse_expr(&e, &mut value)?;
Ok(ast::Stmt::IpyEscapeCommand(
ast::StmtIpyEscapeCommand {
kind,
value,
range: (location..end_location).into()
}
))
}
}
CompoundStatement: ast::Stmt = {
MatchStatement,
IfStatement,
@@ -1732,6 +1806,7 @@ extern {
Dedent => token::Tok::Dedent,
StartModule => token::Tok::StartModule,
StartExpression => token::Tok::StartExpression,
"?" => token::Tok::Question,
"+" => token::Tok::Plus,
"-" => token::Tok::Minus,
"~" => token::Tok::Tilde,
@@ -1825,8 +1900,8 @@ extern {
triple_quoted: <bool>
},
name => token::Tok::Name { name: <String> },
line_magic => token::Tok::MagicCommand {
kind: <MagicKind>,
ipy_escape_command => token::Tok::IpyEscapeCommand {
kind: <IpyEscapeKind>,
value: <String>
},
"\n" => token::Tok::Newline,

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ expression: parse_ast
---
Module(
ModModule {
range: 0..803,
range: 0..929,
body: [
Expr(
StmtExpr {
@@ -31,92 +31,92 @@ Module(
),
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 66..73,
kind: Help2,
value: "a.foo",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 74..80,
kind: Help,
value: "a.foo",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 81..88,
kind: Help,
value: "a.foo",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 89..100,
kind: Help2,
value: "a.foo()",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 115..128,
kind: Magic,
value: "timeit a = b",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 129..147,
kind: Magic,
value: "timeit foo(b) % 3",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 148..176,
kind: Magic,
value: "alias showPath pwd && ls -a",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 177..205,
kind: Magic,
value: "timeit a = foo(b); b = 2",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 206..226,
kind: Magic,
value: "matplotlib --inline",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 227..253,
kind: Magic,
value: "matplotlib --inline",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 277..309,
kind: Shell,
value: "pwd && ls -a | sed 's/^/\\ /'",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 310..347,
kind: Shell,
value: "pwd && ls -a | sed 's/^/\\\\ /'",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 348..393,
kind: ShCap,
value: "cd /Users/foo/Library/Application\\ Support/",
@@ -176,22 +176,22 @@ Module(
],
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 656..664,
kind: Paren,
value: "foo 1 2",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 665..673,
kind: Quote2,
value: "foo 1 2",
},
),
LineMagic(
StmtLineMagic {
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 674..682,
kind: Quote,
value: "foo 1 2",
@@ -199,31 +199,31 @@ Module(
),
For(
StmtFor {
range: 701..727,
range: 711..737,
is_async: false,
target: Name(
ExprName {
range: 705..706,
range: 715..716,
id: "a",
ctx: Store,
},
),
iter: Call(
ExprCall {
range: 710..718,
range: 720..728,
func: Name(
ExprName {
range: 710..715,
range: 720..725,
id: "range",
ctx: Load,
},
),
arguments: Arguments {
range: 715..718,
range: 725..728,
args: [
Constant(
ExprConstant {
range: 716..717,
range: 726..727,
value: Int(
5,
),
@@ -236,9 +236,9 @@ Module(
},
),
body: [
LineMagic(
StmtLineMagic {
range: 724..727,
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 734..737,
kind: Shell,
value: "ls",
},
@@ -249,19 +249,19 @@ Module(
),
Assign(
StmtAssign {
range: 729..738,
range: 739..748,
targets: [
Name(
ExprName {
range: 729..731,
range: 739..741,
id: "p1",
ctx: Store,
},
),
],
value: LineMagic(
ExprLineMagic {
range: 734..738,
value: IpyEscapeCommand(
ExprIpyEscapeCommand {
range: 744..748,
kind: Shell,
value: "pwd",
},
@@ -270,25 +270,25 @@ Module(
),
AnnAssign(
StmtAnnAssign {
range: 739..753,
range: 749..763,
target: Name(
ExprName {
range: 739..741,
range: 749..751,
id: "p2",
ctx: Store,
},
),
annotation: Name(
ExprName {
range: 743..746,
range: 753..756,
id: "str",
ctx: Load,
},
),
value: Some(
LineMagic(
ExprLineMagic {
range: 749..753,
IpyEscapeCommand(
ExprIpyEscapeCommand {
range: 759..763,
kind: Shell,
value: "pwd",
},
@@ -299,53 +299,102 @@ Module(
),
Assign(
StmtAssign {
range: 754..774,
range: 764..784,
targets: [
Name(
ExprName {
range: 754..757,
range: 764..767,
id: "foo",
ctx: Store,
},
),
],
value: LineMagic(
ExprLineMagic {
range: 760..774,
value: IpyEscapeCommand(
ExprIpyEscapeCommand {
range: 770..784,
kind: Magic,
value: "foo bar",
},
),
},
),
LineMagic(
StmtLineMagic {
range: 776..781,
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 786..791,
kind: Magic,
value: " foo",
},
),
Assign(
StmtAssign {
range: 782..803,
range: 792..813,
targets: [
Name(
ExprName {
range: 782..785,
range: 792..795,
id: "foo",
ctx: Store,
},
),
],
value: LineMagic(
ExprLineMagic {
range: 788..803,
value: IpyEscapeCommand(
ExprIpyEscapeCommand {
range: 798..813,
kind: Magic,
value: "foo # comment",
},
),
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 838..842,
kind: Help,
value: "foo",
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 843..852,
kind: Help2,
value: "foo.bar",
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 853..865,
kind: Help,
value: "foo.bar.baz",
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 866..874,
kind: Help2,
value: "foo[0]",
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 875..885,
kind: Help,
value: "foo[0][1]",
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 886..905,
kind: Help2,
value: "foo.bar[0].baz[1]",
},
),
IpyEscapeCommand(
StmtIpyEscapeCommand {
range: 906..929,
kind: Help2,
value: "foo.bar[0].baz[2].egg",
},
),
],
},
)

View File

@@ -6,7 +6,7 @@
//! [CPython source]: https://github.com/python/cpython/blob/dfc2e065a2e71011017077e549cd2f9bf4944c54/Include/internal/pycore_token.h;
use crate::Mode;
use num_bigint::BigInt;
use ruff_python_ast::MagicKind;
use ruff_python_ast::IpyEscapeKind;
use ruff_text_size::TextSize;
use std::fmt;
@@ -44,13 +44,13 @@ pub enum Tok {
/// Whether the string is triple quoted.
triple_quoted: bool,
},
/// Token value for a Jupyter magic commands. These are filtered out of the token stream
/// prior to parsing when the mode is [`Mode::Jupyter`].
MagicCommand {
/// Token value for IPython escape commands. These are recognized by the lexer
/// only when the mode is [`Mode::Jupyter`].
IpyEscapeCommand {
/// The magic command value.
value: String,
/// The kind of magic command.
kind: MagicKind,
kind: IpyEscapeKind,
},
/// Token value for a comment. These are filtered out of the token stream prior to parsing.
Comment(String),
@@ -64,6 +64,8 @@ pub enum Tok {
/// Token value for a dedent.
Dedent,
EndOfFile,
/// Token value for a question mark `?`. This is only used in [`Mode::Jupyter`].
Question,
/// Token value for a left parenthesis `(`.
Lpar,
/// Token value for a right parenthesis `)`.
@@ -232,7 +234,7 @@ impl fmt::Display for Tok {
let quotes = "\"".repeat(if *triple_quoted { 3 } else { 1 });
write!(f, "{kind}{quotes}{value}{quotes}")
}
MagicCommand { kind, value } => write!(f, "{kind}{value}"),
IpyEscapeCommand { kind, value } => write!(f, "{kind}{value}"),
Newline => f.write_str("Newline"),
NonLogicalNewline => f.write_str("NonLogicalNewline"),
Indent => f.write_str("Indent"),
@@ -240,6 +242,7 @@ impl fmt::Display for Tok {
StartModule => f.write_str("StartProgram"),
StartExpression => f.write_str("StartExpression"),
EndOfFile => f.write_str("EOF"),
Question => f.write_str("'?'"),
Lpar => f.write_str("'('"),
Rpar => f.write_str("')'"),
Lsqb => f.write_str("'['"),
@@ -447,8 +450,8 @@ pub enum TokenKind {
Complex,
/// Token value for a string.
String,
/// Token value for a Jupyter magic command.
MagicCommand,
/// Token value for a IPython escape command.
EscapeCommand,
/// Token value for a comment. These are filtered out of the token stream prior to parsing.
Comment,
/// Token value for a newline.
@@ -461,6 +464,8 @@ pub enum TokenKind {
/// Token value for a dedent.
Dedent,
EndOfFile,
/// Token value for a question mark `?`.
Question,
/// Token value for a left parenthesis `(`.
Lpar,
/// Token value for a right parenthesis `)`.
@@ -776,13 +781,14 @@ impl TokenKind {
Tok::Float { .. } => TokenKind::Float,
Tok::Complex { .. } => TokenKind::Complex,
Tok::String { .. } => TokenKind::String,
Tok::MagicCommand { .. } => TokenKind::MagicCommand,
Tok::IpyEscapeCommand { .. } => TokenKind::EscapeCommand,
Tok::Comment(_) => TokenKind::Comment,
Tok::Newline => TokenKind::Newline,
Tok::NonLogicalNewline => TokenKind::NonLogicalNewline,
Tok::Indent => TokenKind::Indent,
Tok::Dedent => TokenKind::Dedent,
Tok::EndOfFile => TokenKind::EndOfFile,
Tok::Question => TokenKind::Question,
Tok::Lpar => TokenKind::Lpar,
Tok::Rpar => TokenKind::Rpar,
Tok::Lsqb => TokenKind::Lsqb,

View File

@@ -585,7 +585,7 @@ impl<'a> Imported<'a> for FromImport<'a> {
}
/// A wrapper around an import [`BindingKind`] that can be any of the three types of imports.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, is_macro::Is)]
pub enum AnyImport<'a> {
Import(&'a Import<'a>),
SubmoduleImport(&'a SubmoduleImport<'a>),

View File

@@ -590,14 +590,26 @@ impl<'a> SemanticModel<'a> {
// print(pa.csv.read_csv("test.csv"))
// ```
let import = self.bindings[binding_id].as_any_import()?;
if !import.is_import() {
return None;
}
// Grab, e.g., `pyarrow` from `import pyarrow as pa`.
let call_path = import.call_path();
let segment = call_path.last()?;
if *segment == symbol {
return None;
}
// Locate the submodule import (e.g., `pyarrow.csv`) that `pa` aliases.
let binding_id = self.scopes[scope_id].get(segment)?;
if !self.bindings[binding_id].kind.is_submodule_import() {
let submodule = &self.bindings[binding_id].as_any_import()?;
if !submodule.is_submodule_import() {
return None;
}
// Ensure that the submodule import and the aliased import are from the same module.
if import.module_name() != submodule.module_name() {
return None;
}

View File

@@ -20,6 +20,7 @@ use ruff::rules::{
};
use ruff::settings::configuration::Configuration;
use ruff::settings::options::Options;
use ruff::settings::types::PythonVersion;
use ruff::settings::{defaults, flags, Settings};
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
@@ -134,7 +135,7 @@ impl Workspace {
line_length: Some(LineLength::default()),
select: Some(defaults::PREFIXES.to_vec()),
tab_size: Some(TabSize::default()),
target_version: Some(defaults::TARGET_VERSION),
target_version: Some(PythonVersion::default()),
// Ignore a bunch of options that don't make sense in a single-file editor.
cache_dir: None,
exclude: None,

View File

@@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.283
rev: v0.0.284
hooks:
- id: ruff
```

View File

@@ -22,7 +22,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.283
rev: v0.0.284
hooks:
- id: ruff
```
@@ -32,7 +32,7 @@ Or, to enable autofix:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.283
rev: v0.0.284
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
@@ -43,7 +43,7 @@ Or, to run the hook on Jupyter Notebooks too:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.283
rev: v0.0.284
hooks:
- id: ruff
types_or: [python, pyi, jupyter]

View File

@@ -5,7 +5,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.283"
version = "0.0.284"
description = "An extremely fast Python linter, written in Rust."
authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]
maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]

4
ruff.schema.json generated
View File

@@ -376,7 +376,7 @@
]
},
"line-length": {
"description": "The line length to use when enforcing long-lines violations (like `E501`).",
"description": "The line length to use when enforcing long-lines violations (like `E501`). Must be greater than `0`.",
"anyOf": [
{
"$ref": "#/definitions/LineLength"
@@ -2741,7 +2741,7 @@
"description": "The size of a tab.",
"type": "integer",
"format": "uint8",
"minimum": 0.0
"minimum": 1.0
},
"Version": {
"type": "string"