Skip to content

Commit

Permalink
Merge pull request #9 from hija/add-precommit
Browse files Browse the repository at this point in the history
Added pre-commit
  • Loading branch information
hija authored Sep 23, 2024
2 parents 24e0a6d + 403e70b commit ea24aed
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 108 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ jobs:
uses: asottile/workflows/.github/workflows/[email protected]
with:
env: '["py39", "py310", "py311", "py312", "py313"]'
os: ubuntu-latest
os: ubuntu-latest
22 changes: 22 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: double-quote-string-fixer
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.13.0
hooks:
- id: reorder-python-imports
args: [--py39-plus]
- repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0
hooks:
- id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade
rev: v3.17.0
hooks:
- id: pyupgrade
args: [--py39-plus]
2 changes: 1 addition & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
entry: clean-dotenv
language: python
always_run: true
pass_filenames: false
pass_filenames: false
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
- Allow values for named keys to be kept (#5)

* v0.0.6
- Preserve comments in the produced .env.example file (#3)
- Preserve comments in the produced .env.example file (#3)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ S3_BUCKET_NAME=""
```

[^1]: https://www.zdnet.com/article/botnets-have-been-silently-mass-scanning-the-internet-for-unsecured-env-files/

## Installation

```bash
Expand Down
2 changes: 1 addition & 1 deletion clean_dotenv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

from clean_dotenv._main import main

if __name__ == "__main__":
if __name__ == '__main__':
raise SystemExit(main())
35 changes: 18 additions & 17 deletions clean_dotenv/_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import argparse
import os
from collections.abc import Iterator

import clean_dotenv._parser as DotEnvParser


Expand All @@ -10,36 +11,36 @@ def _clean_env(path_to_env: str, values_to_keep: list[str] = []):
dotenv_elements = DotEnvParser.parse_stream(open(path_to_env))

# Create new filename for the .env file --> test.env becomes test.env.example
path_to_example_file = path_to_env + ".example"
path_to_example_file = path_to_env + '.example'

# Write .example file
with open(path_to_example_file, "w") as example_env_f:
with open(path_to_example_file, 'w') as example_env_f:
# We now iterate through the original .env file and write everything except for the value into the new file
for i, dotenv_element in enumerate(dotenv_elements):
if dotenv_element.multiline_whitespace:
print(dotenv_element.multiline_whitespace, end="", file=example_env_f)
print(dotenv_element.multiline_whitespace, end='', file=example_env_f)
if dotenv_element.export: # e.g. export AWS_KEY=...
print(dotenv_element.export, end="", file=example_env_f)
print(dotenv_element.export, end='', file=example_env_f)
if dotenv_element.key:
print(
(
f"{dotenv_element.key}={dotenv_element.separator}{dotenv_element.value}{dotenv_element.separator}"
if dotenv_element.key in values_to_keep
else f"{dotenv_element.key}={dotenv_element.separator}{dotenv_element.separator}"
),
end="",
end='',
file=example_env_f,
)
if dotenv_element.comment:
print(dotenv_element.comment, end="", file=example_env_f)
print(dotenv_element.comment, end='', file=example_env_f)
if dotenv_element.end_of_line:
print(dotenv_element.end_of_line, end="", file=example_env_f)
print(dotenv_element.end_of_line, end='', file=example_env_f)


def _find_dotenv_files(path_to_root: str) -> Iterator[str]:
# Finds and yields .env files in the path_to_root
for entry in os.scandir(path_to_root):
if entry.name.endswith(".env") and entry.is_file():
if entry.name.endswith('.env') and entry.is_file():
# Create a cleaned .env.example file for the found .env file
yield entry.path

Expand All @@ -53,25 +54,25 @@ def _main(path_to_root: str, values_to_keep: list[str] = []):

def main():
parser = argparse.ArgumentParser(
description="Automatically creates an .env.example which creates the same keys as your .env file, but without the values"
description='Automatically creates an .env.example which creates the same keys as your .env file, but without the values',
)
parser.add_argument(
"--root_path",
'--root_path',
type=str,
help="Root path in which .env files shall be looked for",
help='Root path in which .env files shall be looked for',
default=os.getcwd(),
)
parser.add_argument(
"-k",
"--keep",
nargs="*",
help="Variables which shall not be cleaned by clean-dotenv. Separate values by space.",
'-k',
'--keep',
nargs='*',
help='Variables which shall not be cleaned by clean-dotenv. Separate values by space.',
default=[],
)

args = parser.parse_args()
_main(path_to_root=args.root_path, values_to_keep=args.keep)


if __name__ == "__main__":
if __name__ == '__main__':
raise SystemExit(main())
63 changes: 28 additions & 35 deletions clean_dotenv/_parser.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
# This is the original python-dotenv parser with minor changes to work in clean-dotenv
# Find the copyright and license below:

# Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv)

# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:

# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.

# - Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.

# - Neither the name of django-dotenv nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
Expand All @@ -28,35 +22,34 @@
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import codecs
import re
from typing import (
IO,
NamedTuple,
Optional,
)
from collections.abc import Iterator, Sequence
from re import Match, Pattern
from collections.abc import Iterator
from collections.abc import Sequence
from re import Match
from re import Pattern
from typing import IO
from typing import NamedTuple
from typing import Optional


def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:
return re.compile(string, re.UNICODE | extra_flags)


_newline = make_regex(r"(\r\n|\n|\r)")
_multiline_whitespace = make_regex(r"(\s*)", extra_flags=re.MULTILINE)
_whitespace = make_regex(r"([^\S\r\n]*)")
_export = make_regex(r"(export[^\S\r\n]+)?")
_newline = make_regex(r'(\r\n|\n|\r)')
_multiline_whitespace = make_regex(r'(\s*)', extra_flags=re.MULTILINE)
_whitespace = make_regex(r'([^\S\r\n]*)')
_export = make_regex(r'(export[^\S\r\n]+)?')
_single_quoted_key = make_regex(r"'([^']+)'")
_unquoted_key = make_regex(r"([^=\#\s]+)")
_equal_sign = make_regex(r"(=[^\S\r\n]*)")
_unquoted_key = make_regex(r'([^=\#\s]+)')
_equal_sign = make_regex(r'(=[^\S\r\n]*)')
_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
_unquoted_value = make_regex(r"([^\r\n]*)")
_comment = make_regex(r"([^\S\r\n]*#[^\r\n]*)?")
_end_of_line = make_regex(r"[^\S\r\n]*(\r\n|\n|\r|$)")
_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
_unquoted_value = make_regex(r'([^\r\n]*)')
_comment = make_regex(r'([^\S\r\n]*#[^\r\n]*)?')
_end_of_line = make_regex(r'[^\S\r\n]*(\r\n|\n|\r|$)')
_rest_of_line = make_regex(r'[^\r\n]*(?:\r|\n|\r\n)?')
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
_single_quote_escapes = make_regex(r"\\[\\']")

Expand Down Expand Up @@ -84,10 +77,10 @@ def __init__(self, chars: int, line: int) -> None:
self.line = line

@classmethod
def start(cls) -> "Position":
def start(cls) -> 'Position':
return cls(chars=0, line=1)

def set(self, other: "Position") -> None:
def set(self, other: 'Position') -> None:
self.chars = other.chars
self.line = other.line

Expand Down Expand Up @@ -124,28 +117,28 @@ def peek(self, count: int) -> str:
def read(self, count: int) -> str:
result = self.string[self.position.chars : self.position.chars + count]
if len(result) < count:
raise Error("read: End of string")
raise Error('read: End of string')
self.position.advance(result)
return result

def read_regex(self, regex: Pattern[str]) -> Sequence[str]:
match = regex.match(self.string, self.position.chars)
if match is None:
raise Error("read_regex: Pattern not found")
raise Error('read_regex: Pattern not found')
self.position.advance(self.string[match.start() : match.end()])
return match.groups()


def decode_escapes(regex: Pattern[str], string: str) -> str:
def decode_match(match: Match[str]) -> str:
return codecs.decode(match.group(0), "unicode-escape") # type: ignore
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore

return regex.sub(decode_match, string)


def parse_key(reader: Reader) -> Optional[str]:
char = reader.peek(1)
if char == "#":
if char == '#':
return None
elif char == "'":
(key,) = reader.read_regex(_single_quoted_key)
Expand All @@ -156,7 +149,7 @@ def parse_key(reader: Reader) -> Optional[str]:

def parse_unquoted_value(reader: Reader) -> str:
(part,) = reader.read_regex(_unquoted_value)
return re.sub(r"\s+#.*", "", part).rstrip()
return re.sub(r'\s+#.*', '', part).rstrip()


def parse_value(reader: Reader) -> tuple[str, str]:
Expand All @@ -167,10 +160,10 @@ def parse_value(reader: Reader) -> tuple[str, str]:
elif char == '"':
(value,) = reader.read_regex(_double_quoted_value)
return decode_escapes(_double_quote_escapes, value), '"'
elif char in ("", "\n", "\r"):
return "", ""
elif char in ('', '\n', '\r'):
return '', ''
else:
return parse_unquoted_value(reader), ""
return parse_unquoted_value(reader), ''


def parse_binding(reader: Reader) -> Binding:
Expand All @@ -192,7 +185,7 @@ def parse_binding(reader: Reader) -> Binding:
(export,) = reader.read_regex(_export)
key = parse_key(reader)
reader.read_regex(_whitespace)
if reader.peek(1) == "=":
if reader.peek(1) == '=':
reader.read_regex(_equal_sign)
value, separator = parse_value(reader)
else:
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-r requirements.txt
pytest
coverage
covdefaults
covdefaults
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python-dotenv
python-dotenv
Loading

0 comments on commit ea24aed

Please sign in to comment.