Skip to content

Commit d8fd35f

Browse files
authored
Merge pull request #60 from nginxinc/add-better-formatter
Added crossplane.format
2 parents de68daf + a76d633 commit d8fd35f

File tree

7 files changed

+122
-20
lines changed

7 files changed

+122
-20
lines changed

crossplane/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from .parser import parse
33
from .lexer import lex
44
from .builder import build
5+
from .formatter import format
56
from .ext.lua import LuaBlockPlugin
67

7-
__all__ = ['parse', 'lex', 'build']
8+
__all__ = ['parse', 'lex', 'build', 'format']
89

910
__title__ = 'crossplane'
1011
__summary__ = 'Reliable and fast NGINX configuration file parser.'

crossplane/__main__.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .lexer import lex as lex_file
1010
from .parser import parse as parse_file
1111
from .builder import build as build_string, build_files, _enquote, DELIMITERS
12-
from .errors import NgxParserBaseException
12+
from .formatter import format as format_file
1313
from .compat import json, input
1414

1515

@@ -123,15 +123,9 @@ def minify(filename, out):
123123
out.write('\n')
124124

125125

126-
def format(filename, out, indent=None, tabs=False):
127-
payload = parse_file(filename)
128-
parsed = payload['config'][0]['parsed']
129-
if payload['status'] == 'ok':
130-
output = build_string(parsed, indent=indent, tabs=tabs) + '\n'
131-
out.write(output)
132-
else:
133-
e = payload['errors'][0]
134-
raise NgxParserBaseException(e['error'], e['file'], e['line'])
126+
def format(filename, out, indent=4, tabs=False):
127+
output = format_file(filename, indent=indent, tabs=tabs)
128+
out.write(output + '\n')
135129

136130

137131
class _SubparserHelpFormatter(RawDescriptionHelpFormatter):

crossplane/analyzer.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,9 @@ def enter_block_ctx(stmt, ctx):
19021902
return ctx + (stmt['directive'],)
19031903

19041904

1905-
def analyze(fname, stmt, term, ctx=(), strict=False):
1905+
def analyze(fname, stmt, term, ctx=(), strict=False, check_ctx=True,
1906+
check_args=True):
1907+
19061908
directive = stmt['directive']
19071909
line = stmt['line']
19081910

@@ -1920,13 +1922,16 @@ def analyze(fname, stmt, term, ctx=(), strict=False):
19201922
n_args = len(args)
19211923

19221924
masks = DIRECTIVES[directive]
1923-
ctx_mask = CONTEXTS[ctx]
19241925

19251926
# if this directive can't be used in this context then throw an error
1926-
masks = [mask for mask in masks if mask & ctx_mask]
1927-
if not masks:
1928-
reason = '"%s" directive is not allowed here' % directive
1929-
raise NgxParserDirectiveContextError(reason, fname, line)
1927+
if check_ctx:
1928+
masks = [mask for mask in masks if mask & CONTEXTS[ctx]]
1929+
if not masks:
1930+
reason = '"%s" directive is not allowed here' % directive
1931+
raise NgxParserDirectiveContextError(reason, fname, line)
1932+
1933+
if not check_args:
1934+
return
19301935

19311936
valid_flag = lambda x: x.lower() in ('on', 'off')
19321937

crossplane/formatter.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# -*- coding: utf-8 -*-
2+
from .errors import NgxParserBaseException
3+
from .builder import build
4+
from .parser import parse
5+
6+
7+
def format(filename, indent=4, tabs=False):
8+
payload = parse(filename, single=True, check_ctx=False, check_args=False)
9+
10+
if payload['status'] != 'ok':
11+
e = payload['errors'][0]
12+
raise NgxParserBaseException(e['error'], e['file'], e['line'])
13+
14+
parsed = payload['config'][0]['parsed']
15+
output = build(parsed, indent=indent, tabs=tabs)
16+
return output

crossplane/parser.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ def _prepare_if_args(stmt):
2323

2424

2525
def parse(filename, onerror=None, catch_errors=True, ignore=(), single=False,
26-
comments=False, strict=False, combine=False):
26+
comments=False, strict=False, combine=False, check_ctx=True,
27+
check_args=True):
2728
"""
2829
Parses an nginx config file and returns a nested dict payload
29-
30+
3031
:param filename: string contianing the name of the config file to parse
3132
:param onerror: function that determines what's saved in "callback"
3233
:param catch_errors: bool; if False, parse stops after first error
@@ -35,6 +36,8 @@ def parse(filename, onerror=None, catch_errors=True, ignore=(), single=False,
3536
:param single: bool; if True, including from other files doesn't happen
3637
:param comments: bool; if True, including comments to json payload
3738
:param strict: bool; if True, unrecognized directives raise errors
39+
:param check_ctx: bool; if True, runs context analysis on directives
40+
:param check_args: bool; if True, runs arg count analysis on directives
3841
:returns: a payload that describes the parsed nginx config
3942
"""
4043
config_dir = os.path.dirname(filename)
@@ -131,7 +134,10 @@ def _parse(parsing, tokens, ctx=(), consume=False):
131134

132135
try:
133136
# raise errors if this statement is invalid
134-
analyze(fname, stmt, token, ctx, strict=strict)
137+
analyze(
138+
fname=fname, stmt=stmt, term=token, ctx=ctx, strict=strict,
139+
check_ctx=check_ctx, check_args=check_args
140+
)
135141
except NgxParserDirectiveError as e:
136142
if catch_errors:
137143
_handle_error(parsing, e)

tests/configs/bad-args/nginx.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
user;
2+
events {}
3+
http {}

tests/test_format.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# -*- coding: utf-8 -*-
2+
import os
3+
4+
import crossplane
5+
from . import here
6+
7+
8+
def test_format_messy_config():
9+
dirname = os.path.join(here, 'configs', 'messy')
10+
config = os.path.join(dirname, 'nginx.conf')
11+
output = crossplane.format(config)
12+
assert output == '\n'.join([
13+
'user nobody;',
14+
'events {',
15+
' worker_connections 2048;',
16+
'}',
17+
'http {',
18+
' access_log off;',
19+
' default_type text/plain;',
20+
' error_log off;',
21+
' server {',
22+
' listen 8083;',
23+
r""" return 200 'Ser" \' \' ver\\ \ $server_addr:\$server_port\n\nTime: $time_local\n\n';""",
24+
' }',
25+
' server {',
26+
' listen 8080;',
27+
' root /usr/share/nginx/html;',
28+
" location ~ '/hello/world;' {",
29+
' return 301 /status.html;',
30+
' }',
31+
' location /foo {',
32+
' }',
33+
' location /bar {',
34+
' }',
35+
' location /\{\;\}\ #\ ab {',
36+
' }',
37+
' if ($request_method = P\{O\)\###\;ST) {',
38+
' }',
39+
' location /status.html {',
40+
" try_files '/abc/${uri} /abc/${uri}.html' =404;",
41+
' }',
42+
r" location '/sta;\n tus' {",
43+
' return 302 /status.html;',
44+
' }',
45+
' location /upstream_conf {',
46+
' return 200 /status.html;',
47+
' }',
48+
' }',
49+
' server {',
50+
' }',
51+
'}'
52+
])
53+
54+
55+
def test_format_not_main_file():
56+
dirname = os.path.join(here, 'configs', 'includes-globbed', 'servers')
57+
config = os.path.join(dirname, 'server1.conf')
58+
output = crossplane.format(config)
59+
assert output == '\n'.join([
60+
'server {',
61+
' listen 8080;',
62+
' include locations/*.conf;',
63+
'}'
64+
])
65+
66+
67+
def test_format_args_not_analyzed():
68+
dirname = os.path.join(here, 'configs', 'bad-args')
69+
config = os.path.join(dirname, 'nginx.conf')
70+
output = crossplane.format(config)
71+
assert output == '\n'.join([
72+
'user;',
73+
'events {',
74+
'}',
75+
'http {',
76+
'}'
77+
])

0 commit comments

Comments
 (0)