Skip to content

Add type annotations at cost of support of py2.7 #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/crossplane-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", pypy-3.6, pypy-3.7, pypy-3.8, pypy-3.9]
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", pypy-3.6, pypy-3.7, pypy-3.8, pypy-3.9]

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ Contributors
* Ivan Poluyanov <[email protected]> `@poluyanov <https://github.com/poluyanov>`_
* Raymond Lau <[email protected]> `@Raymond26 <https://github.com/Raymond26>`_
* Luca Comellini <[email protected]> `@lucacome <https://github.com/lucacome>`_
* Ron Vider <[email protected]> `@RonVider <https://github.com/RonVider>`_
* Ron Vider <[email protected]> `@RonVider <https://github.com/RonVider>`_
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ members of the project's leadership.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org
[homepage]: https://www.contributor-covenant.org
1 change: 0 additions & 1 deletion NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

55 changes: 30 additions & 25 deletions crossplane/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import io
import os
import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
import typing as t
from argparse import ArgumentParser, Action, RawDescriptionHelpFormatter, Namespace
from traceback import format_exception

from . import __version__
Expand All @@ -12,34 +13,37 @@
from .builder import build as build_string, build_files, _enquote, DELIMITERS
from .formatter import format as format_file
from .compat import json, input
from .typedefs import DictStatement


def _prompt_yes():
def _prompt_yes() -> bool:
try:
return input('overwrite? (y/n [n]) ').lower().startswith('y')
except (KeyboardInterrupt, EOFError):
sys.exit(1)


def _dump_payload(obj, fp, indent):
kwargs = {'indent': indent}
def _dump_payload(obj: t.Any, fp: t.TextIO, indent: t.Optional[int]) -> None:
kwargs: t.Dict[str, t.Any] = {'indent': indent}
if indent is None:
kwargs['separators'] = ',', ':'
fp.write(json.dumps(obj, **kwargs) + u'\n')


def parse(filename, out, indent=None, catch=None, tb_onerror=None, ignore='',
single=False, comments=False, strict=False, combine=False):
def parse(filename: str, out: str, indent: t.Optional[int] = None,
catch: t.Optional[bool] = None, tb_onerror: t.Optional[bool] = None,
ignore: str = '', single: bool = False, comments: bool = False,
strict: bool = False, combine: bool = False) -> None:

ignore = ignore.split(',') if ignore else []
ignored: t.List[str] = ignore.split(',') if ignore else []

def callback(e):
def callback(e: Exception) -> str:
exc = sys.exc_info() + (10,)
return ''.join(format_exception(*exc)).rstrip()
return ''.join(format_exception(*exc)).rstrip() # type: ignore[call-overload, arg-type, unused-ignore]

kwargs = {
kwargs: t.Dict[str, t.Any] = {
'catch_errors': catch,
'ignore': ignore,
'ignore': ignored,
'combine': combine,
'single': single,
'comments': comments,
Expand All @@ -57,8 +61,9 @@ def callback(e):
o.close()


def build(filename, dirname=None, force=False, indent=4, tabs=False,
header=True, stdout=False, verbose=False):
def build(filename: str, dirname: t.Optional[str] = None, force: bool = False,
indent: int = 4, tabs: bool = False, header: bool = True,
stdout: bool = False, verbose: bool = False) -> None:

if dirname is None:
dirname = os.getcwd()
Expand Down Expand Up @@ -108,8 +113,8 @@ def build(filename, dirname=None, force=False, indent=4, tabs=False,
print('wrote to ' + path)


def lex(filename, out, indent=None, line_numbers=False):
payload = list(lex_file(filename))
def lex(filename: str, out: str, indent: t.Optional[int] = None, line_numbers: bool = False) -> None:
payload: t.List[t.Any] = list(lex_file(filename))
if line_numbers:
payload = [(token, lineno) for token, lineno, quoted in payload]
else:
Expand All @@ -121,7 +126,7 @@ def lex(filename, out, indent=None, line_numbers=False):
o.close()


def minify(filename, out):
def minify(filename: str, out: str) -> None:
payload = parse_file(
filename,
single=True,
Expand All @@ -132,7 +137,7 @@ def minify(filename, out):
strict=False
)
o = sys.stdout if out is None else io.open(out, 'w', encoding='utf-8')
def write_block(block):
def write_block(block: t.List[DictStatement]) -> None:
for stmt in block:
o.write(_enquote(stmt['directive']))
if stmt['directive'] == 'if':
Expand All @@ -152,7 +157,7 @@ def write_block(block):
o.close()


def format(filename, out, indent=4, tabs=False):
def format(filename: str, out: str, indent: int = 4, tabs: bool = False) -> None:
output = format_file(filename, indent=indent, tabs=tabs)
o = sys.stdout if out is None else io.open(out, 'w', encoding='utf-8')
try:
Expand All @@ -162,7 +167,7 @@ def format(filename, out, indent=4, tabs=False):


class _SubparserHelpFormatter(RawDescriptionHelpFormatter):
def _format_action(self, action):
def _format_action(self, action: Action) -> str:
line = super(RawDescriptionHelpFormatter, self)._format_action(action)

if action.nargs == 'A...':
Expand All @@ -175,7 +180,7 @@ def _format_action(self, action):
return line


def parse_args(args=None):
def parse_args(args: t.Optional[t.List[str]] = None) -> Namespace:
parser = ArgumentParser(
formatter_class=_SubparserHelpFormatter,
description='various operations for nginx config files',
Expand All @@ -184,7 +189,7 @@ def parse_args(args=None):
parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
subparsers = parser.add_subparsers(title='commands')

def create_subparser(function, help):
def create_subparser(function: t.Callable[..., None], help: str) -> ArgumentParser:
name = function.__name__
prog = 'crossplane ' + name
p = subparsers.add_parser(name, prog=prog, help=help, description=help)
Expand Down Expand Up @@ -231,11 +236,11 @@ def create_subparser(function, help):
g.add_argument('-i', '--indent', type=int, metavar='NUM', help='number of spaces to indent output', default=4)
g.add_argument('-t', '--tabs', action='store_true', help='indent with tabs instead of spaces')

def help(command):
if command not in parser._actions[-1].choices:
def help(command: str) -> None:
if command not in t.cast(t.Dict[str, t.Any], parser._actions[-1].choices):
parser.error('unknown command %r' % command)
else:
parser._actions[-1].choices[command].print_help()
t.cast(t.Dict[str, t.Any], parser._actions[-1].choices)[command].print_help()

p = create_subparser(help, 'show help for commands')
p.add_argument('command', help='command to show help for')
Expand All @@ -249,7 +254,7 @@ def help(command):
return parsed


def main():
def main() -> None:
kwargs = parse_args().__dict__
func = kwargs.pop('_subcommand')
func(**kwargs)
Expand Down
16 changes: 9 additions & 7 deletions crossplane/analyzer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
import typing as t
from .errors import (
NgxParserDirectiveUnknownError,
NgxParserDirectiveContextError,
NgxParserDirectiveArgumentsError
)
from .typedefs import StatusType, DictResponse, DictFile, DictStatement

# bit masks for different directive argument styles
NGX_CONF_NOARGS = 0x00000001 # 0 args
Expand Down Expand Up @@ -67,7 +69,7 @@
Since some directives can have different behaviors in different contexts, we
use lists of bit masks, each describing a valid way to use the directive.

Definitions for directives that're available in the open source version of
Definitions for directives that're available in the open source version of
nginx were taken directively from the source code. In fact, the variable
names for the bit masks defined above were taken from the nginx source code.

Expand Down Expand Up @@ -2111,7 +2113,7 @@
}


def enter_block_ctx(stmt, ctx):
def enter_block_ctx(stmt: DictStatement, ctx: t.Tuple[str, ...]) -> t.Tuple[str, ...]:
# don't nest because NGX_HTTP_LOC_CONF just means "location block in http"
if ctx and ctx[0] == 'http' and stmt['directive'] == 'location':
return ('http', 'location')
Expand All @@ -2120,8 +2122,8 @@ def enter_block_ctx(stmt, ctx):
return ctx + (stmt['directive'],)


def analyze(fname, stmt, term, ctx=(), strict=False, check_ctx=True,
check_args=True):
def analyze(fname: str, stmt: DictStatement, term: str, ctx: t.Tuple[str, ...] = (), strict: bool = False, check_ctx: bool = True,
check_args: bool = True) -> None:

directive = stmt['directive']
line = stmt['line']
Expand Down Expand Up @@ -2151,7 +2153,7 @@ def analyze(fname, stmt, term, ctx=(), strict=False, check_ctx=True,
if not check_args:
return

valid_flag = lambda x: x.lower() in ('on', 'off')
valid_flag: t.Callable[[str], bool] = lambda x: x.lower() in ('on', 'off')

# do this in reverse because we only throw errors at the end if no masks
# are valid, and typically the first bit mask is what the parser expects
Expand Down Expand Up @@ -2181,7 +2183,7 @@ def analyze(fname, stmt, term, ctx=(), strict=False, check_ctx=True,
raise NgxParserDirectiveArgumentsError(reason % directive, fname, line)


def register_external_directives(directives):
for directive, bitmasks in directives.iteritems():
def register_external_directives(directives: t.Dict[str, t.List[int]]) -> None:
for directive, bitmasks in directives.items():
if bitmasks:
DIRECTIVES[directive] = bitmasks
29 changes: 18 additions & 11 deletions crossplane/builder.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
# -*- coding: utf-8 -*-
import typing as t
import codecs
import os
import re

from .compat import PY2
from .typedefs import StatusType, DictResponse, DictFile, DictStatement

if t.TYPE_CHECKING:
MatchBytes = re.Match[bytes]
else:
MatchBytes = re.Match

ExtBuilderType = t.Callable[[DictStatement, str, int, bool], str]
DELIMITERS = ('{', '}', ';')
EXTERNAL_BUILDERS = {}
EXTERNAL_BUILDERS: t.Dict[str, ExtBuilderType] = {}
ESCAPE_SEQUENCES_RE = re.compile(r'(\\x[0-9a-f]{2}|\\[0-7]{1,3})')


def _escape(string):
def _escape(string: str) -> t.Generator[str, None, None]:
prev, char = '', ''
for char in string:
if prev == '\\' or prev + char == '${':
Expand All @@ -26,7 +34,7 @@ def _escape(string):
yield char


def _needs_quotes(string):
def _needs_quotes(string: str) -> bool:
if string == '':
return True

Expand All @@ -50,12 +58,11 @@ def _needs_quotes(string):

return char in ('\\', '$') or expanding


def _replace_escape_sequences(match):
return match.group(1).decode('string-escape')
def _replace_escape_sequences(match: MatchBytes) -> str:
return t.cast(str, match.group(1).decode('string-escape'))


def _enquote(arg):
def _enquote(arg: str) -> str:
if not _needs_quotes(arg):
return arg

Expand All @@ -71,7 +78,7 @@ def _enquote(arg):
return arg


def build(payload, indent=4, tabs=False, header=False):
def build(payload: t.List[DictStatement], indent: int = 4, tabs: bool = False, header: bool = False) -> str:
padding = '\t' if tabs else ' ' * indent

head = ''
Expand All @@ -81,7 +88,7 @@ def build(payload, indent=4, tabs=False, header=False):
head += '# https://github.com/nginxinc/crossplane/issues\n'
head += '\n'

def _build_block(output, block, depth, last_line):
def _build_block(output: str, block: t.List[DictStatement], depth: int, last_line: int) -> str:
margin = padding * depth

for stmt in block:
Expand Down Expand Up @@ -123,7 +130,7 @@ def _build_block(output, block, depth, last_line):
return head + body


def build_files(payload, dirname=None, indent=4, tabs=False, header=False):
def build_files(payload: DictResponse, dirname: t.Optional[str] = None, indent: int = 4, tabs: bool = False, header: bool = False) -> None:
"""
Uses a full nginx config payload (output of crossplane.parse) to build
config files, then writes those files to disk.
Expand All @@ -149,6 +156,6 @@ def build_files(payload, dirname=None, indent=4, tabs=False, header=False):
fp.write(output)


def register_external_builder(builder, directives):
def register_external_builder(builder: ExtBuilderType, directives: t.Iterable[str]) -> None:
for directive in directives:
EXTERNAL_BUILDERS[directive] = builder
9 changes: 6 additions & 3 deletions crossplane/compat.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
import typing as t
import functools
import sys

try:
import simplejson as json
except ImportError:
import json
import json # type: ignore[no-redef]

PY2 = (sys.version_info[0] == 2)
PY3 = (sys.version_info[0] == 3)
Expand All @@ -18,17 +19,19 @@
basestring = str


def fix_pep_479(generator):
def fix_pep_479(generator: t.Any) -> t.Any:
"""
Python 3.7 breaks crossplane's lexer because of PEP 479
Read more here: https://www.python.org/dev/peps/pep-0479/
"""
@functools.wraps(generator)
def _wrapped_generator(*args, **kwargs):
def _wrapped_generator(*args: t.Any, **kwargs: t.Any) -> t.Generator[t.Any, None, None]:
try:
for x in generator(*args, **kwargs):
yield x
except RuntimeError:
return

return _wrapped_generator

__all__ = ['PY2', 'PY3', 'input', 'basestring', 'fix_pep_479', 'json']
5 changes: 3 additions & 2 deletions crossplane/errors.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
import typing as t


class NgxParserBaseException(Exception):
def __init__(self, strerror, filename, lineno):
def __init__(self, strerror: str, filename: t.Optional[str], lineno: t.Optional[int]) -> None:
self.args = (strerror, filename, lineno)
self.filename = filename
self.lineno = lineno
self.strerror = strerror

def __str__(self):
def __str__(self) -> str:
if self.lineno is not None:
return '%s in %s:%s' % self.args
else:
Expand Down
Loading