Skip to content

Commit d4b7c78

Browse files
committed
Ruff linting
This includes updating the type hints to the Python 3.10 format. We can use the new format and still maintain compatibility with Python 3.8 by using a `__future__` import.
1 parent b44c394 commit d4b7c78

20 files changed

+1065
-864
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
Compatible with GDMC-HTTP **>=1.6.0, <2.0.0** and Minecraft **1.21.4**.
44

5+
**Fixes:**
6+
- The re-exports in the `gdpc` top-level package are now formatted in a more standard way, which may prevent issues with static analysis tools when importing them.
7+
58

69
# 8.1.0
710

pyproject.toml

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,77 @@ exclude = [
1414
"**/__pycache__",
1515
"**/site-packages/nbt/**",
1616
]
17-
stubPath = "src/stubs"
18-
typeCheckingMode = "strict"
1917
ignore = [
2018
"**/site-packages/nbt/**",
2119
]
20+
stubPath = "src/stubs"
21+
typeCheckingMode = "strict"
22+
reportUnusedImport = "warning"
2223

2324

2425
[tool.mypy]
2526
mypy_path = "src/stubs"
27+
28+
29+
[tool.ruff]
30+
target-version = "py38" # If we move the project setup from setup.py to pyproject.toml, replace this with project.requires-python. https://docs.astral.sh/ruff/settings/#target-version
31+
line-length = 100
32+
33+
[tool.ruff.lint]
34+
select = ["ALL"]
35+
ignore = [
36+
# Reason for ignoring:
37+
# DI: Disagree - I disagree with this rule in nearly all cases.
38+
# AL: Alternative - The rule is incompatible with an alternative rule.
39+
# TB: Too broad - The rule is sometimes useful, but gives too many false positives.
40+
# PA: Public API - The fix may require changing a public API, making this rule unusable in many cases.
41+
# HI: Hint - The rule would be useful at hint-severity, if ruff supported that.
42+
# PR: Project-specific - There is a project-specific reason to not use this rule.
43+
44+
# Mainly DI
45+
"RUF010", # explicit-f-string-type-conversion - DI - Explicit conversion is more readable. See also https://peps.python.org/pep-0498/#s-r-and-a-are-redundant
46+
47+
# Mainly AL
48+
"D203", # incorrect-blank-line-before-class - AL - Disabled in favor of D211.
49+
"D213", # multi-line-summary-second-line - AL - Disabled in favor of D212.
50+
51+
# Mainly TB
52+
"ANN401", # any-type - TB - Any can be useful for very generic interfaces or as an acceptable temporary solution.
53+
"C90", # mccabe - TB - May be hard to fix, and real issues can be spotted manually.
54+
"D105", # undocumented-magic-method - TB - Many magic methods are self-explanatory.
55+
"D202", # blank-line-after-function - TB - Adding a blank line can improve readability when the docstring is short.
56+
"D204", # incorrect-blank-line-after-class - TB - The blank line can be unnecessary for very short classes.
57+
"D205", # missing-blank-line-after-summary - TB - A single line is sometimes too limiting, and most tools can handle multiple lines.
58+
"D209", # new-line-after-last-paragraph - TB - It's a bit nasty, but breaking this rule can help compactify short docstrings.
59+
"D301", # escape-sequence-in-docstring - TB - It's a bit nasty, but breaking this rule with "\n" can help compactify short docstrings.
60+
"D400", # missing-trailing-period - TB - Other punctuation is also acceptable. We use D415 instead.
61+
"D401", # non-imperative-mood - TB - This is very controversial. I personally prefer the descriptive mood in most cases.
62+
"D404", # docstring-starts-with-this - TB - There are valid usages of "this", such as when referring to a method's object.
63+
"E501", # line-too-long - TB - Lines occasionally need to be longer, such as when a comment contains a long link.
64+
"E701", # multiple-statements - TB - Single-line if-statements can be more readable in certain scenarios.
65+
"E741", # ambiguous-variable-name - TB - "l" is often fine, and the others may occasionally be fine as well.
66+
"FBT001", # boolean-type-hint-positional-argument - TB, PA, and mostly redundant with FBT003.
67+
"FBT002", # boolean-default-value-positional-argument - TB, PA, and mostly redundant with FBT003.
68+
"ISC003", # explicit-string-concatenation - TB - In cases where some lines are literals and some expressions, it is clearer to use + everywhere.
69+
"PERF203", # try-except-in-loop - TB - The try-except can often not be taken out of the loop.
70+
"PLR091", # too-many-* - TB, PA - Real issues can be spotted manually.
71+
"PLR2004", # magic-value-comparison - TB - Although this rule is often right, there are also many cases where a constant is overkill.
72+
"PLW2901", # redefined-loop-name - TB - Reuse of the loop variable can help keep names simple. This rule does have a strong case, though.
73+
"PTH", # flake8-use-pathlib - TB, HI - Old-style path manipulation is acceptable, and occasionally even more readable.
74+
"S311", # suspicious-non-cryptographic-random-usage - TB - Not all random usages are cryptographic.
75+
76+
# Mainly HI
77+
"FIX", # flake8-fixme - HI
78+
"TD", # flake8-todos - HI
79+
80+
# Mainly PR
81+
"N802", # invalid-function-name - PR, PA - For historical reasons, GDPC uses camelCase.
82+
"N803", # invalid-argument-name - PR, PA - for historical reasons, GDPC uses camelCase.
83+
"N806", # non-lowercase-variable-in-function - PR - For historical reasons, GDPC uses camelCase.
84+
"N815", # mixed-case-variable-in-class-scope - PR, PA - For historical reasons, GDPC uses camelCase.
85+
"N816", # mixed-case-variable-in-global-scope - PR, PA - For historical reasons, GDPC uses camelCase.
86+
"PGH003", # blanket-type-ignore - PR - GDPC uses blanket type ignores for pyright-strict-mode unknown-type errors when accessing NBT data.
87+
]
88+
89+
[tool.ruff.lint.flake8-builtins]
90+
ignorelist = ["id"]

release-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Steps to make a release:
88
- In `__init__.py`, update `__version__` to the new version number.
99
- Change the "In development" header in the changelog to the new version number, and add a new "In development" section with only the compatibility line.
1010
- If necessary, update the compatible GDMC-HTTP and Minecraft versions listed on the installation page of the docs and in the changelog.
11-
- If necessary, update the minimum Python version listed on the installation page of the docs.
11+
- If necessary, update the minimum Python version listed in setup.py, pyproject.toml, and on the installation page of the docs.
1212
- Commit the changes above with the title "Updated version to X.X.X".
1313
- Push changes to GitHub (things will be public from here).
1414

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ def get_metadata(name: str) -> str:
1616
# __{name}__ = "{value}"
1717
delim = '"' if '"' in line else "'"
1818
return line.split(delim)[1]
19-
raise RuntimeError(f"Unable to find {dunderString} value.")
19+
msg = f"Unable to find {dunderString} value."
20+
raise RuntimeError(msg)
2021

2122

22-
with open("README.md", "r", encoding="utf-8") as readme:
23+
with open("README.md", encoding="utf-8") as readme:
2324
long_description = readme.read()
2425

2526

@@ -73,5 +74,5 @@ def get_metadata(name: str) -> str:
7374
"Chat about it on Discord": "https://discord.gg/YwpPCRQWND",
7475
"Source": "https://github.com/avdstaaij/gdpc",
7576
},
76-
zip_safe=False
77+
zip_safe=False,
7778
)

src/gdpc/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
GDPC top-level package.
1+
"""GDPC top-level package.
32
43
Several important classes are re-exported here, so you can more easily import them.
54
For example, you can use :python:`from gdpc import Editor` instead of :python:`from gdpc.editor import Editor`.
@@ -65,8 +64,9 @@
6564
__version__ = "8.1.0"
6665

6766

68-
from .vector_tools import Rect, Box # pyright: ignore [reportUnusedImport]
69-
from .transform import Transform # pyright: ignore [reportUnusedImport]
70-
from .block import Block # pyright: ignore [reportUnusedImport]
71-
from .world_slice import WorldSlice # pyright: ignore [reportUnusedImport]
72-
from .editor import Editor # pyright: ignore [reportUnusedImport]
67+
# ruff: noqa: I001
68+
from .vector_tools import Rect as Rect, Box as Box # noqa: PLC0414
69+
from .transform import Transform as Transform # noqa: PLC0414
70+
from .block import Block as Block # noqa: PLC0414
71+
from .world_slice import WorldSlice as WorldSlice # noqa: PLC0414
72+
from .editor import Editor as Editor # noqa: PLC0414

src/gdpc/block.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
"""Provides the :class:`.Block` class, which represents a Minecraft block."""
22

33

4-
from typing import Union, Optional, List, Dict, Sequence, cast
5-
from dataclasses import dataclass, field
4+
from __future__ import annotations
5+
66
from copy import deepcopy
7+
from dataclasses import dataclass, field
8+
from typing import TYPE_CHECKING, Sequence, cast
79

8-
from pyglm.glm import bvec3
910
from nbt import nbt
11+
from pyglm.glm import bvec3
1012

11-
from .vector_tools import Vec3bLike
13+
from .block_state_tools import transformAxis, transformFacing, transformHalf, transformRotation
1214
from .nbt_tools import nbtToSnbt
13-
from .block_state_tools import transformAxis, transformFacing, transformRotation, transformHalf
15+
16+
if TYPE_CHECKING:
17+
from .vector_tools import Vec3bLike
1418

1519

1620
@dataclass
@@ -38,27 +42,29 @@ class Block:
3842
# TODO: Known orientation-related block states that are currently not supported:
3943
# - type="bottom"/"top" (e.g. slabs) (note that slabs can also have type="double"!)
4044

41-
id: Optional[str] = "minecraft:stone" #: Block ID
42-
states: Dict[str, str] = field(default_factory=dict) #: Block states
43-
data: Optional[str] = None #: Block entity data
45+
id: str | None = "minecraft:stone" #: Block ID
46+
states: dict[str, str] = field(default_factory=lambda: cast("dict[str, str]", dict)) #: Block states
47+
data: str | None = None #: Block entity data
4448

4549
# We explicitly add this method instead of using @dataclass so that it looks better in the docs
4650
# and we can add a docstring.
4751
def __init__(
4852
self,
49-
id: Optional[str] = "minecraft:stone", # pylint: disable=redefined-builtin
50-
states: Optional[Dict[str, str]] = None,
51-
data: Optional[str] = None
53+
id: str | None = "minecraft:stone", # pylint: disable=redefined-builtin
54+
states: dict[str, str] | None = None,
55+
data: str | None = None,
5256
) -> None:
5357
"""Constructs a Block instance with the given properties."""
5458
self.id = id
5559
self.states = states if states is not None else {}
5660
self.data = data
5761

5862

59-
def transform(self, rotation: int = 0, flip: Vec3bLike = bvec3()) -> None:
63+
def transform(self, rotation: int = 0, flip: Vec3bLike | None = None) -> None:
6064
"""Transforms this block.\n
6165
Flips first, rotates second."""
66+
if flip is None:
67+
flip = bvec3()
6268
axisState = self.states.get("axis")
6369
facingState = self.states.get("facing")
6470
rotationState = self.states.get("rotation")
@@ -69,9 +75,11 @@ def transform(self, rotation: int = 0, flip: Vec3bLike = bvec3()) -> None:
6975
if halfState is not None: self.states["half"] = transformHalf (halfState, flip)
7076

7177

72-
def transformed(self, rotation: int = 0, flip: Vec3bLike = bvec3()) -> "Block":
78+
def transformed(self, rotation: int = 0, flip: Vec3bLike | None = None) -> Block:
7379
"""Returns a transformed copy of this block.\n
7480
Flips first, rotates second."""
81+
if flip is None:
82+
flip = bvec3()
7583
block = deepcopy(self)
7684
block.transform(rotation, flip)
7785
return block
@@ -84,12 +92,16 @@ def stateString(self) -> str:
8492

8593

8694
def __str__(self) -> str:
95+
"""Returns a string representation in the technical "block state" format.\n
96+
For example: "minecraft:oak_log[axis=z]"\n
97+
More info: https://minecraft.wiki/w/Argument_types#block_state."""
8798
if not self.id:
8899
return ""
89100
return self.id + self.stateString() + (self.data if self.data else "")
90101

91102

92103
def __repr__(self) -> str:
104+
"""Returns a string representation that is guaranteed to `eval()` to this Block."""
93105
# This is used for model dumping; it needs to return a string that eval()'s to this Block.
94106
# The default repr includes unnecessary default values, which make model dumps way larger
95107
# than they need to be.
@@ -102,13 +114,13 @@ def __repr__(self) -> str:
102114

103115

104116
@staticmethod
105-
def fromBlockStateTag(blockStateTag: nbt.TAG_Compound, blockEntityTag: Optional[nbt.TAG_Compound] = None) -> "Block":
117+
def fromBlockStateTag(blockStateTag: nbt.TAG_Compound, blockEntityTag: nbt.TAG_Compound | None = None) -> Block:
106118
"""Parses a block state compound tag (as found in chunk palettes) into a Block.\n
107119
If ``blockEntityTag`` is provided, it is parsed into the Block's :attr:`.data` attribute."""
108120
block = Block(str(blockStateTag["Name"]))
109121

110122
if "Properties" in blockStateTag:
111-
for tag in cast(List[nbt.TAG_String], cast(nbt.TAG_Compound, blockStateTag["Properties"]).tags):
123+
for tag in cast("list[nbt.TAG_String]", cast("nbt.TAG_Compound", blockStateTag["Properties"]).tags):
112124
block.states[str(tag.name)] = str(tag.value)
113125

114126
if blockEntityTag is not None:
@@ -121,9 +133,8 @@ def fromBlockStateTag(blockStateTag: nbt.TAG_Compound, blockEntityTag: Optional[
121133
return block
122134

123135

124-
def transformedBlockOrPalette(block: Union[Block, Sequence[Block]], rotation: int, flip: Vec3bLike) -> Union[Block, Sequence[Block]]:
136+
def transformedBlockOrPalette(block: Block | Sequence[Block], rotation: int, flip: Vec3bLike) -> Block | Sequence[Block]:
125137
"""Convenience function that transforms a block or a palette of blocks."""
126138
if isinstance(block, Block):
127139
return block.transformed(rotation, flip)
128-
else:
129-
return [b.transformed(rotation, flip) for b in block]
140+
return [b.transformed(rotation, flip) for b in block]

src/gdpc/block_state_tools.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
"""
66

77

8-
from pyglm.glm import ivec3, bvec3
8+
from __future__ import annotations
99

10-
from .vector_tools import Vec3iLike, Vec3bLike
10+
from typing import TYPE_CHECKING
11+
12+
from pyglm.glm import bvec3, ivec3
13+
14+
if TYPE_CHECKING:
15+
from .vector_tools import Vec3bLike, Vec3iLike
1116

1217

1318
# ==================================================================================================
@@ -88,7 +93,8 @@ def vectorToAxis(vec: Vec3iLike) -> str:
8893
try:
8994
return __VECTOR_TO_AXIS[v]
9095
except KeyError as e:
91-
raise ValueError("Exactly one vector component must be non-zero") from e
96+
msg = "Exactly one vector component must be non-zero"
97+
raise ValueError(msg) from e
9298

9399

94100
__AXIS_TO_VECTOR = {
@@ -123,7 +129,8 @@ def vectorToFacing(vec: Vec3iLike) -> str:
123129
try:
124130
return __VECTOR_TO_FACING[v]
125131
except KeyError as e:
126-
raise ValueError("Exactly one vector component must be non-zero") from e
132+
msg = "Exactly one vector component must be non-zero"
133+
raise ValueError(msg) from e
127134

128135

129136
__FACING_TO_VECTOR = {
@@ -202,9 +209,11 @@ def flipFacing(facing: str, flip: Vec3bLike) -> str:
202209
return facing
203210

204211

205-
def transformFacing(facing: str, rotation: int = 0, flip: Vec3bLike = bvec3()) -> str:
212+
def transformFacing(facing: str, rotation: int = 0, flip: Vec3bLike | None = None) -> str:
206213
"""Returns the transformed "facing" block state string.\n
207214
Flips first, rotates second."""
215+
if flip is None:
216+
flip = bvec3()
208217
return rotateFacing(flipFacing(facing, flip), rotation)
209218

210219

@@ -226,11 +235,13 @@ def invertFacing(facing: str) -> str:
226235

227236

228237
def rotateRotation(blockStateRotation: str, rotation: int) -> str:
229-
"""Returns the rotated "rotation" block state string.\n
238+
"""Returns the rotated "rotation" block state string.
239+
230240
Yes, this name is confusing. ``blockStateRotation`` denotes a value of the "rotation" block
231241
state, as used by e.g. signs. ``rotation`` denotes a rotation as used by GDPC's transformation
232242
system, so one of {0,1,2,3}. This function name is consistent with the other block state
233-
rotation functions."""
243+
rotation functions.
244+
"""
234245
return str((int(blockStateRotation) + 4*rotation) % 16)
235246

236247

@@ -244,9 +255,11 @@ def flipRotation(rotation: str, flip: Vec3bLike) -> str:
244255
return str(rotationInt)
245256

246257

247-
def transformRotation(blockStateRotation: str, rotation: int = 0, flip: Vec3bLike = bvec3()) -> str:
258+
def transformRotation(blockStateRotation: str, rotation: int = 0, flip: Vec3bLike | None = None) -> str:
248259
"""Returns the transformed "rotation" block state string.\n
249260
Flips first, rotates second."""
261+
if flip is None:
262+
flip = bvec3()
250263
return rotateRotation(flipRotation(blockStateRotation, flip), rotation)
251264

252265

@@ -274,6 +287,8 @@ def flipHalf(half: str, flip: Vec3bLike) -> str:
274287
return invertHalf(half) if flip[1] else half
275288

276289

277-
def transformHalf(half: str, flip: Vec3bLike = bvec3()) -> str:
290+
def transformHalf(half: str, flip: Vec3bLike | None = None) -> str:
278291
"""Returns the transformed "half" block state string."""
292+
if flip is None:
293+
flip = bvec3()
279294
return flipHalf(half, flip)

0 commit comments

Comments
 (0)