Skip to content

Commit 1449593

Browse files
committed
Initial commit
0 parents  commit 1449593

13 files changed

+401
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.egg-info
2+
*.pyc
3+
/.mypy_cache
4+
/.pytest_cache
5+
/.coverage
6+
/.tox
7+
/venv*

.pre-commit-config.yaml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v2.5.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-docstring-first
8+
- id: check-yaml
9+
- id: debug-statements
10+
- id: requirements-txt-fixer
11+
- id: double-quote-string-fixer
12+
- id: name-tests-test
13+
- repo: https://gitlab.com/pycqa/flake8
14+
rev: 3.8.0
15+
hooks:
16+
- id: flake8
17+
- repo: https://github.com/pre-commit/mirrors-autopep8
18+
rev: v1.5.2
19+
hooks:
20+
- id: autopep8
21+
- repo: https://github.com/asottile/reorder_python_imports
22+
rev: v2.3.0
23+
hooks:
24+
- id: reorder-python-imports
25+
args: [--py3-plus]
26+
- repo: https://github.com/asottile/add-trailing-comma
27+
rev: v2.0.1
28+
hooks:
29+
- id: add-trailing-comma
30+
args: [--py36-plus]
31+
- repo: https://github.com/asottile/pyupgrade
32+
rev: v2.4.1
33+
hooks:
34+
- id: pyupgrade
35+
args: [--py36-plus]
36+
- repo: https://github.com/pre-commit/mirrors-mypy
37+
rev: v0.770
38+
hooks:
39+
- id: mypy

LICENSE

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2019 Anthony Sottile
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.flake8-2020?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=27&branchName=master)
2+
[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/27/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=27&branchName=master)
3+
4+
flake8-2020
5+
===========
6+
7+
flake8 plugin which checks for misuse of `sys.version` or `sys.version_info`
8+
9+
this will become a problem when `python3.10` or `python4.0` exists (presumably
10+
during the year 2020).
11+
12+
you might also find an early build of [python3.10] useful
13+
14+
[python3.10]: https://github.com/asottile/python3.10
15+
16+
## installation
17+
18+
`pip install flake8-2020`
19+
20+
## flake8 codes
21+
22+
| Code | Description |
23+
|--------|--------------------------------------------------------|
24+
| YTT101 | `sys.version[:3]` referenced (python3.10) |
25+
| YTT102 | `sys.version[2]` referenced (python3.10) |
26+
| YTT103 | `sys.version` compared to string (python3.10) |
27+
| YTT201 | `sys.version_info[0] == 3` referenced (python4) |
28+
| YTT202 | `six.PY3` referenced (python4) |
29+
| YTT203 | `sys.version_info[1]` compared to integer (python4) |
30+
| YTT204 | `sys.version_info.minor` compared to integer (python4) |
31+
| YTT301 | `sys.version[0]` referenced (python10) |
32+
| YTT302 | `sys.version` compared to string (python10) |
33+
| YTT303 | `sys.version[:1]` referenced (python10) |
34+
35+
## rationale
36+
37+
lots of code incorrectly references the `sys.version` and `sys.version_info`
38+
members. in particular, this will cause some issues when the version of python
39+
after python3.9 is released. my current recommendation is 3.10 since I believe
40+
it breaks less code, here's a few patterns that will cause issues:
41+
42+
```python
43+
# in python3.10 this will report as '3.1' (should be '3.10')
44+
python_version = sys.version[:3] # YTT101
45+
# in python3.10 this will report as '1' (should be '10')
46+
py_minor = sys.version[2]
47+
# in python3.10 this will be False (which goes against developer intention)
48+
sys.version >= '3.5' # YTT103
49+
50+
51+
# correct way to do this
52+
python_version = '{}.{}'.format(*sys.version_info)
53+
py_minor = str(sys.version_info[1])
54+
sys.version_info >= (3, 5)
55+
```
56+
57+
```python
58+
# in python4 this will report as `False` (and suddenly run python2 code!)
59+
is_py3 = sys.version_info[0] == 3 # YTT201
60+
61+
# in python4 this will report as `False` (six violates YTT201!)
62+
if six.PY3: # YTT202
63+
print('python3!')
64+
65+
if sys.version_info[0] >= 3 and sys.version_info[1] >= 5: # YTT203
66+
print('py35+')
67+
68+
if sys.version_info.major >= 3 and sys.version_info.minor >= 6: # YTT204
69+
print('py36+')
70+
71+
# correct way to do this
72+
is_py3 = sys.version_info >= (3,)
73+
74+
if not six.PY2:
75+
print('python3!')
76+
77+
if sys.version_info >= (3, 5):
78+
print('py35+')
79+
80+
if sys.version_info >= (3, 6):
81+
print('py36+')
82+
```
83+
84+
```python
85+
# in python10 this will report as '1'
86+
python_major_version = sys.version[0] # YTT301
87+
# in python10 this will be False
88+
if sys.version >= '3': # YTT302
89+
print('python3!')
90+
# in python10 this will be False
91+
if sys.version[:1] >= '3': # YTT303
92+
print('python3!')
93+
94+
95+
# correct way to do this
96+
python_major_version = str(sys.version_info[0])
97+
98+
if sys.version_info >= (3,):
99+
print('python3!')
100+
101+
if sys.version_info >= (3,):
102+
print('python3!')
103+
```
104+
105+
## as a pre-commit hook
106+
107+
See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions
108+
109+
Sample `.pre-commit-config.yaml`:
110+
111+
```yaml
112+
- repo: https://gitlab.com/pycqa/flake8
113+
rev: 3.7.8
114+
hooks:
115+
- id: flake8
116+
additional_dependencies: [flake8-2020==1.6.0]
117+
```

azure-pipelines.yml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
trigger:
2+
branches:
3+
include: [master, test-me-*]
4+
tags:
5+
include: ['*']
6+
7+
resources:
8+
repositories:
9+
- repository: asottile
10+
type: github
11+
endpoint: github
12+
name: asottile/azure-pipeline-templates
13+
ref: refs/tags/v1.0.0
14+
15+
jobs:
16+
- template: job--pre-commit.yml@asottile
17+
- template: job--python-tox.yml@asottile
18+
parameters:
19+
toxenvs: [py36, py37, py38]
20+
os: linux

flake8_strftime.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import ast
2+
import re
3+
import sys
4+
from typing import Any
5+
from typing import Dict
6+
from typing import Generator
7+
from typing import List
8+
from typing import Tuple
9+
from typing import Type
10+
11+
if sys.version_info < (3, 8): # pragma: no cover (<PY38)
12+
import importlib_metadata
13+
else: # pragma: no cover (PY38+)
14+
import importlib.metadata as importlib_metadata
15+
16+
STRFTIME001 = 'STRFTIME001 Linux-specific strftime code used.' # noqa: E501
17+
STRFTIME002 = 'STRFTIME002 Windows-specific strftime code used.' # noqa: E501
18+
19+
20+
class Visitor(ast.NodeVisitor):
21+
def __init__(self) -> None:
22+
self.errors: List[Tuple[int, int, str]] = []
23+
self._from_imports: Dict[str, str] = {}
24+
25+
def visit_Str(self, node):
26+
for match in re.finditer(r"%-[dmHIMSj]", node.s):
27+
self.errors.append((
28+
node.lineno,
29+
node.col_offset + match.span()[0],
30+
STRFTIME001,
31+
))
32+
33+
for match in re.finditer(r"%#[dmHIMSj]", node.s):
34+
print(match)
35+
self.errors.append((
36+
node.lineno,
37+
node.col_offset + match.span()[0],
38+
STRFTIME002,
39+
))
40+
41+
42+
class Plugin:
43+
name = __name__
44+
version = importlib_metadata.version(__name__)
45+
46+
def __init__(self, tree: ast.AST):
47+
self._tree = tree
48+
49+
def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
50+
visitor = Visitor()
51+
visitor.visit(self._tree)
52+
53+
for line, col, msg in visitor.errors:
54+
yield line, col, msg, type(self)

git_helper.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
modname: flake8_strftime
2+
repo_name: flake8_strftime
3+
copyright_years: "2020"
4+
author: "Dominic Davis-Foster"
5+
6+
version: "0.0.0"
7+
username: "domdfcoding"
8+
license: 'MIT'
9+
short_desc: "Check for use of platform specific strftime codes using Flake8."
10+
11+
conda_channels:
12+
- domdfcoding
13+
- conda-forge
14+
15+
python_deploy_version: 3.6
16+
17+
# Versions to run tests for
18+
python_versions:
19+
- '3.6'
20+
- '3.7'
21+
- "3.8"
22+
- "3.9-dev"
23+
- "pypy3"
24+
25+
# travis secure password for PyPI
26+
travis_pypi_secure: ""
27+
28+
# directory that contains tests
29+
tests_dir: "tests"
30+
31+
classifiers:
32+
- 'Development Status :: 4 - Beta'
33+
# - "Development Status :: 5 - Production/Stable"
34+
# - "Development Status :: 6 - Mature"
35+
# - "Development Status :: 7 - Inactive"
36+
- 'Intended Audience :: Developers'
37+
- 'Topic :: Software Development :: Libraries :: Python Modules'
38+
- "Topic :: Utilities"

requirements-dev.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
covdefaults
2+
coverage
3+
pytest

setup.cfg

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[metadata]
2+
name = flake8_strftime
3+
version = 1.6.0
4+
description = flake8 plugin which checks for misuse of `sys.version` or `sys.version_info`
5+
long_description = file: README.md
6+
long_description_content_type = text/markdown
7+
url = https://github.com/asottile/flake8-2020
8+
author = Anthony Sottile
9+
author_email = [email protected]
10+
license = MIT
11+
license_file = LICENSE
12+
classifiers =
13+
License :: OSI Approved :: MIT License
14+
Programming Language :: Python :: 3
15+
Programming Language :: Python :: 3 :: Only
16+
Programming Language :: Python :: 3.6
17+
Programming Language :: Python :: 3.7
18+
Programming Language :: Python :: 3.8
19+
20+
[options]
21+
py_modules = flake8_strftime
22+
install_requires =
23+
flake8>=3.7
24+
importlib-metadata>=0.9;python_version<"3.8"
25+
python_requires = >=3.6.1
26+
27+
[options.entry_points]
28+
flake8.extension =
29+
STRFTIME=flake8_strftime:Plugin
30+
31+
[bdist_wheel]
32+
universal = True
33+
34+
[coverage:run]
35+
plugins = covdefaults
36+
37+
[mypy]
38+
check_untyped_defs = true
39+
disallow_any_generics = true
40+
disallow_incomplete_defs = true
41+
disallow_untyped_defs = true
42+
no_implicit_optional = true
43+
44+
[mypy-testing.*]
45+
disallow_untyped_defs = false
46+
47+
[mypy-tests.*]
48+
disallow_untyped_defs = false

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from setuptools import setup
2+
setup()

tests/__init__.py

Whitespace-only changes.

tests/flake8_strftime_test.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import ast
2+
3+
import pytest
4+
5+
from flake8_strftime import Plugin
6+
7+
8+
def results(s):
9+
return {'{}:{}: {}'.format(*r) for r in Plugin(ast.parse(s)).run()}
10+
11+
12+
13+
14+
15+
def test_linux_specific():
16+
assert results('print(f"{now:%Y/%-m/%-d %H:%M}")') == {
17+
'1:9: STRFTIME001 Linux-specific strftime code used.',
18+
'1:13: STRFTIME001 Linux-specific strftime code used.',
19+
}
20+
21+
assert results('print(now.strftime("%Y/%-m/%-d %H:%M"))') == {
22+
'1:22: STRFTIME001 Linux-specific strftime code used.',
23+
'1:26: STRFTIME001 Linux-specific strftime code used.',
24+
}
25+
26+
27+
def test_windows_specific():
28+
assert results('print(f"{now:%Y/%#m/%#d %H:%M}")') == {
29+
'1:9: STRFTIME002 Windows-specific strftime code used.',
30+
'1:13: STRFTIME002 Windows-specific strftime code used.',
31+
}
32+
33+
assert results('print(now.strftime("%Y/%#m/%#d %H:%M"))') == {
34+
'1:22: STRFTIME002 Windows-specific strftime code used.',
35+
'1:26: STRFTIME002 Windows-specific strftime code used.',
36+
}
37+

0 commit comments

Comments
 (0)