Repository: artiomn/markdown_articles_tool (language: python)

Issue:
Markdown articles on pypi
Thanks @artiomn for the awesome project! Just what I was looking for.

Would it be possible to make this project available on [pypi](https://pypi.org/)? 


Discussion / hints:
Possibly, but not anytime soon. You could help, if you want. Unfortunately I'm very busy right now and if I would add this to pypy I have to write tests and do some refactoring.

Fix patch:
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index d05da21..6526bc0 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -1,7 +1,7 @@
 # This workflow will install Python dependencies, run tests and lint with a single version of Python
 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
 
-name: Python application
+name: Python package
 
 on:
   push:
@@ -34,3 +34,12 @@ jobs:
 #    - name: Test with pytest
 #      run: |
 #        pytest
+    - name: Install build dependencies
+      run: python -m pip install -U setuptools wheel build
+    - name: Build
+      run: python -m build .
+#    - name: Publish
+#      uses: pypa/gh-action-pypi-publish@master
+#      with:
+#        password: ${{ secrets.pypi_password }}
+#        skip_existing: true
diff --git a/.gitignore b/.gitignore
index 3f4f6d3..9166a70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
 # Created by https://www.gitignore.io/api/vim,python
 # Edit at https://www.gitignore.io/?templates=vim,python
 
+*.md
+^README.md
+
 ### Python ###
 # Byte-compiled / optimized / DLL files
 __pycache__/
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..540b720
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
\ No newline at end of file
diff --git a/README.md b/README.md
index e342a9c..6d92cd0 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ Deduplication can replace not similar images. Probability of this is very low, b
 
 ## Installation
 
-You need Python 3.7+.  
+You need Python 3.8+.
 Run:
 
 ```
@@ -48,9 +48,7 @@ pip3 install -r markdown_articles_tool/requirements.txt
 Syntax:
 
 ```
-usage: markdown_tool.py [-h] [-D {disabled,names_hashing,content_hash}] [-d IMAGES_DIRNAME] [-a] [-s SKIP_LIST]
-       [-i {md,html,md+html,html+md}] [-l] [-n] [-o {md,html}] [-p IMAGES_PUBLIC_PATH] [-R]
-       [-t DOWNLOADING_TIMEOUT] [-O OUTPUT_PATH] [--version] article_file_path_or_url
+usage: markdown_tool.py [-h] [-D {disabled,names_hashing,content_hash}] [-d IMAGES_DIRNAME] [-a] [-s SKIP_LIST] [-i {md,html,md+html,html+md}] [-l] [-n] [-o {md,html}] [-p IMAGES_PUBLIC_PATH] [-R] [-t DOWNLOADING_TIMEOUT] [-O OUTPUT_PATH] [--version] article_file_path_or_url
 
 Script to download images and replace image links in markdown documents.
 
diff --git a/markdown_tool.py b/markdown_tool.py
index 4b94450..54b8e0f 100755
--- a/markdown_tool.py
+++ b/markdown_tool.py
@@ -5,82 +5,22 @@
 """
 
 import argparse
-from io import StringIO
 from itertools import permutations
-from pathlib import Path
-from string import Template
 
 from mimetypes import types_map
-from time import strftime
-from typing import List
 
-from pkg.transformers.md.transformer import ArticleTransformer as MarkdownArticleTransformer
-from pkg.transformers.html.transformer import ArticleTransformer as HTMLArticleTransformer
-from pkg.image_downloader import ImageDownloader, DeduplicationVariant
-from pkg.www_tools import is_url, get_base_url, get_filename_from_url, download_from_url
-from pkg.formatters.simple import SimpleFormatter
-from pkg.formatters.html import HTMLFormatter
+from markdown_toolset.article_processor import ArticleProcessor
+from markdown_toolset.image_downloader import DeduplicationVariant
 
-try:
-    from pkg.formatters.pdf import PDFFormatter
-except ModuleNotFoundError:
-    PDFFormatter = None
+from markdown_toolset.formatters import FORMATTERS
+from markdown_toolset.transformers import TRANSFORMERS
 
+from markdown_toolset.__version__ import __version__
 
-__version__ = '0.0.8'
-
-TRANSFORMERS = [MarkdownArticleTransformer, HTMLArticleTransformer]
-FORMATTERS = [SimpleFormatter, HTMLFormatter, PDFFormatter]
 
 del types_map['.jpe']
 
 
-def transform_article(article_path: str, input_format_list: List[str], img_downloader: ImageDownloader) -> str:
-    """
-    Download images and fix URL's.
-    """
-    transformers = [tr for ifmt in input_format_list
-                    for tr in TRANSFORMERS if tr is not None and tr.format == ifmt]
-
-    with open(article_path, 'r', encoding='utf8') as article_file:
-        result = StringIO(article_file.read())
-
-    for transformer in transformers:
-        lines = transformer(result, img_downloader).run()
-        result = StringIO(''.join(lines))
-
-    return result.read()
-
-
-def get_formatter(output_format: str):
-    formatter = [f for f in FORMATTERS if f is not None and f.format == output_format]
-    assert len(formatter) == 1
-    formatter = formatter[0]
-
-    return formatter
-
-
-def get_article_out_path(article_path: Path, output_path: Path, file_format: str, remove_source: bool) -> Path:
-    article_file_name = article_path.stem
-    article_out_path = output_path if output_path else article_path.parent / f'{article_file_name}.{file_format}'
-
-    if article_path == article_out_path and not remove_source:
-        article_out_path = article_path.parent / f'{article_file_name}_{strftime("%Y%m%d_%H%M%S")}.{file_format}'
-
-    return article_out_path
-
-
-def format_article(article_out_path: Path, article_text: str, formatter) -> None:
-    """
-    Save article in the selected format.
-    """
-
-    print(f'Writing file into "{article_out_path}"...')
-
-    with open(article_out_path, 'wb') as outfile:
-        outfile.write(formatter.write(article_text))
-
-
 def main(arguments):
     """
     Entrypoint.
@@ -88,75 +28,20 @@ def main(arguments):
 
     print(f'Markdown tool version {__version__} started...')
 
-    article_link = arguments.article_file_path_or_url
-    if is_url(article_link):
-        timeout = arguments.downloading_timeout
-        if timeout < 0:
-            timeout = None
-        response = download_from_url(article_link, timeout=timeout)
-        article_path = Path(get_filename_from_url(response))
-        article_base_url = get_base_url(response)
-
-        with open(article_path, 'wb') as article_file:
-            article_file.write(response.content)
-            article_file.close()
-    else:
-        article_path = Path(article_link).expanduser()
-        article_base_url = ''
-
-    skip_list = arguments.skip_list
-    skip_all = arguments.skip_all_incorrect
-
-    print(f'File "{article_path}" will be processed...')
-
-    if isinstance(skip_list, str):
-        if skip_list.startswith('@'):
-            skip_list = skip_list[1:]
-            print(f'Reading skip list from a file "{skip_list}"...')
-            with open(Path(skip_list).expanduser(), 'r') as fsl:
-                skip_list = [s.strip() for s in fsl.readlines()]
-        else:
-            skip_list = [s.strip() for s in skip_list.split(',')]
-
-    article_formatter = get_formatter(arguments.output_format)
-
-    article_out_path = get_article_out_path(
-        article_path=article_path,
-        output_path=Path(arguments.output_path) if arguments.output_path is not None else None,
-        file_format=article_formatter.format,
-        remove_source=arguments.remove_source
-    )
-
-    variables = {
-        'article_name': article_out_path.stem,
-        'time': strftime('%H%M%S'),
-        'date': strftime('%Y%m%d'),
-        'dt': strftime('%Y%m%d_%H%M%S'),
-        'base_url': article_base_url.lstrip('https://').lstrip('http://')
-    }
-
-    print(f'Image public path: {Template(arguments.images_public_path).safe_substitute(**variables)}')
-
-    img_downloader = ImageDownloader(
-        article_path=article_path,
-        article_base_url=article_base_url,
-        skip_list=skip_list,
-        skip_all_errors=skip_all,
-        img_dir_name=Path(Template(arguments.images_dirname).safe_substitute(**variables)),
-        img_public_path=Path(Template(arguments.images_public_path).safe_substitute(**variables)),
-        downloading_timeout=arguments.downloading_timeout,
-        deduplication_variant=getattr(DeduplicationVariant, arguments.deduplication_type.upper()),
-        replace_image_names=arguments.replace_image_names,
-        process_local_images=arguments.process_local_images
-    )
-
-    result = transform_article(article_path, arguments.input_format.split('+'), img_downloader)
-
-    format_article(article_out_path, result, article_formatter)
-
-    if arguments.remove_source and article_path != article_out_path:
-        print(f'Removing source file "{article_path}"...')
-        Path(article_path).unlink()
+    processor = ArticleProcessor(skip_list=arguments.skip_list,
+                                 article_file_path_or_url=arguments.article_file_path_or_url,
+                                 downloading_timeout=arguments.downloading_timeout,
+                                 output_format=arguments.output_format,
+                                 output_path=arguments.output_path,
+                                 remove_source=arguments.remove_source,
+                                 images_public_path=arguments.images_public_path,
+                                 input_formats=arguments.input_format.split('+'),
+                                 skip_all_incorrect=arguments.skip_all_incorrect,
+                                 deduplication_type=getattr(DeduplicationVariant, arguments.deduplication_type.upper()),
+                                 process_local_images=arguments.process_local_images,
+                                 images_dirname=arguments.images_dirname)
+
+    processor.process()
 
     print('Processing finished successfully...')
 
diff --git a/pkg/__init__.py b/markdown_toolset/__init__.py
similarity index 100%
rename from pkg/__init__.py
rename to markdown_toolset/__init__.py
diff --git a/markdown_toolset/__version__.py b/markdown_toolset/__version__.py
new file mode 100644
index 0000000..9123cf0
--- /dev/null
+++ b/markdown_toolset/__version__.py
@@ -0,0 +1,1 @@
+__version__ = '0.0.8'
diff --git a/markdown_toolset/article_processor.py b/markdown_toolset/article_processor.py
new file mode 100644
index 0000000..5fbc460
--- /dev/null
+++ b/markdown_toolset/article_processor.py
@@ -0,0 +1,117 @@
+from pathlib import Path
+from string import Template
+from time import strftime
+from typing import Union, List
+
+from .www_tools import is_url, download_from_url, get_filename_from_url, get_base_url
+from .image_downloader import ImageDownloader, DeduplicationVariant
+from .formatters import FORMATTERS, get_formatter, format_article
+from .transformers import TRANSFORMERS, transform_article
+
+
+class ArticleProcessor:
+    def __init__(self, skip_list: Union[str, List[str]],
+                 article_file_path_or_url: str, downloading_timeout: int,
+                 output_format: str, output_path: Union[Path, str],
+                 remove_source: bool, images_public_path: Union[Path, str],
+                 input_formats: List[str], skip_all_incorrect: bool,
+                 deduplication_type: DeduplicationVariant,
+                 process_local_images: bool, images_dirname: Union[Path, str]):
+        self._skip_list = skip_list
+        self._article_file_path_or_url = article_file_path_or_url
+        self._downloading_timeout = downloading_timeout
+        self._output_format = output_format
+        self._output_path = output_path
+        self._remove_source = remove_source
+        self._images_public_path = images_public_path
+        self._input_formats = input_formats
+        self._skip_all_incorrect = skip_all_incorrect
+        self._deduplication_type = deduplication_type
+        self._process_local_images = process_local_images
+        self._images_dirname = images_dirname
+
+    def process(self):
+        skip_list = self._process_skip_list()
+        article_path, article_base_url = self._get_article()
+
+        print(f'File "{article_path}" will be processed...')
+
+        article_formatter = get_formatter(self._output_format, FORMATTERS)
+
+        article_out_path = self._get_article_out_path(
+            article_path=article_path,
+            output_path=Path(self._output_path) if self._output_path is not None else None,
+            file_format=article_formatter.format
+        )
+
+        variables = {
+            'article_name': article_out_path.stem,
+            'time': strftime('%H%M%S'),
+            'date': strftime('%Y%m%d'),
+            'dt': strftime('%Y%m%d_%H%M%S'),
+            'base_url': article_base_url.lstrip('https://').lstrip('http://')
+        }
+
+        print(f'Image public path: {Template(self._images_public_path).safe_substitute(**variables)}')
+
+        img_downloader = ImageDownloader(
+            article_path=article_path,
+            article_base_url=article_base_url,
+            skip_list=skip_list,
+            skip_all_errors=self._skip_all_incorrect,
+            img_dir_name=Path(Template(self._images_dirname).safe_substitute(**variables)),
+            img_public_path=Path(Template(self._images_public_path).safe_substitute(**variables)),
+            downloading_timeout=self._downloading_timeout,
+            deduplication_variant=self._deduplication_type,
+            process_local_images=self._process_local_images
+        )
+
+        result = transform_article(article_path, self._input_formats, TRANSFORMERS, img_downloader)
+        format_article(article_out_path, result, article_formatter)
+
+        if self._remove_source and article_path != article_out_path:
+            print(f'Removing source file "{article_path}"...')
+            Path(article_path).unlink()
+
+    def _process_skip_list(self):
+        skip_list = self._skip_list
+
+        if isinstance(skip_list, str):
+            if skip_list.startswith('@'):
+                skip_list = skip_list[1:]
+                print(f'Reading skip list from a file "{skip_list}"...')
+                with open(Path(skip_list).expanduser(), 'r') as fsl:
+                    skip_list = [s.strip() for s in fsl.readlines()]
+            else:
+                skip_list = [s.strip() for s in skip_list.split(',')]
+
+        return skip_list
+
+    def _get_article(self):
+        article_link = self._article_file_path_or_url
+
+        if is_url(article_link):
+            timeout = self._downloading_timeout
+            if timeout < 0:
+                timeout = None
+            response = download_from_url(article_link, timeout=timeout)
+            article_path = Path(get_filename_from_url(response))
+            article_base_url = get_base_url(response)
+
+            with open(article_path, 'wb') as article_file:
+                article_file.write(response.content)
+                article_file.close()
+        else:
+            article_path = Path(article_link).expanduser()
+            article_base_url = ''
+
+        return article_path, article_base_url
+
+    def _get_article_out_path(self, article_path: Path, output_path: Path, file_format: str) -> Path:
+        article_file_name = article_path.stem
+        article_out_path = output_path if output_path else article_path.parent / f'{article_file_name}.{file_format}'
+
+        if article_path == article_out_path and not self._remove_source:
+            article_out_path = article_path.parent / f'{article_file_name}_{strftime("%Y%m%d_%H%M%S")}.{file_format}'
+
+        return article_out_path
diff --git a/markdown_toolset/formatters/__init__.py b/markdown_toolset/formatters/__init__.py
new file mode 100644
index 0000000..e3b4d61
--- /dev/null
+++ b/markdown_toolset/formatters/__init__.py
@@ -0,0 +1,12 @@
+from .simple import SimpleFormatter
+from .html import HTMLFormatter
+from .helpers import format_article, get_formatter
+
+try:
+    from .pdf import PDFFormatter
+except ModuleNotFoundError:
+    PDFFormatter = None
+
+
+FORMATTERS = [SimpleFormatter, HTMLFormatter, PDFFormatter]
+__all__ = [FORMATTERS, get_formatter, format_article]
diff --git a/markdown_toolset/formatters/helpers.py b/markdown_toolset/formatters/helpers.py
new file mode 100644
index 0000000..8072a41
--- /dev/null
+++ b/markdown_toolset/formatters/helpers.py
@@ -0,0 +1,21 @@
+from pathlib import Path
+from typing import Any, List
+
+
+def get_formatter(output_format: str, formatters_list: List[Any]):
+    formatter = [f for f in formatters_list if f is not None and f.format == output_format]
+    assert len(formatter) == 1
+    formatter = formatter[0]
+
+    return formatter
+
+
+def format_article(article_out_path: Path, article_text: str, formatter) -> None:
+    """
+    Save article in the selected format.
+    """
+
+    print(f'Writing file into "{article_out_path}"...')
+
+    with open(article_out_path, 'wb') as outfile:
+        outfile.write(formatter.write(article_text))
diff --git a/pkg/formatters/html.py b/markdown_toolset/formatters/html.py
similarity index 100%
rename from pkg/formatters/html.py
rename to markdown_toolset/formatters/html.py
diff --git a/pkg/formatters/pdf.py b/markdown_toolset/formatters/pdf.py
similarity index 100%
rename from pkg/formatters/pdf.py
rename to markdown_toolset/formatters/pdf.py
diff --git a/pkg/formatters/simple.py b/markdown_toolset/formatters/simple.py
similarity index 100%
rename from pkg/formatters/simple.py
rename to markdown_toolset/formatters/simple.py
diff --git a/pkg/image_downloader.py b/markdown_toolset/image_downloader.py
similarity index 96%
rename from pkg/image_downloader.py
rename to markdown_toolset/image_downloader.py
index ebe5148..af69192 100644
--- a/pkg/image_downloader.py
+++ b/markdown_toolset/image_downloader.py
@@ -22,7 +22,7 @@ def __init__(self, article_path: Path, article_base_url: str = '', skip_list: Op
                  skip_all_errors: bool = False, img_dir_name: Path = Path('images'), img_public_path: Path = Path(''),
                  downloading_timeout: float = -1,
                  deduplication_variant: DeduplicationVariant = DeduplicationVariant.DISABLED,
-                 replace_image_names: bool = False, process_local_images: bool = False):
+                 process_local_images: bool = False):
         """
         :parameter article_path: path to the article file.
         :parameter article_base_url: URL to download article.
@@ -35,7 +35,6 @@ def __init__(self, article_path: Path, article_base_url: str = '', skip_list: Op
         :parameter deduplication_variant: type of the deduplication:
                                           NAMES_HASHING - file names will be sha1(image_content).
                                           CONTENT_HASH - first image name will be used.
-        :parameter replace_image_names: if True, image names will be replaced with the hash of the content.
         :parameter process_local_images: if True, local image files will be processed.
         """
 
@@ -49,7 +48,6 @@ def __init__(self, article_path: Path, article_base_url: str = '', skip_list: Op
         self._skip_all_errors = skip_all_errors
         self._downloading_timeout = downloading_timeout if downloading_timeout > 0 else None
         self._deduplication_variant = deduplication_variant
-        self._replace_image_names = replace_image_names
         self._process_local_images = process_local_images
 
     def download_images(self, images: List[str]) -> dict:
diff --git a/pkg/string_tools.py b/markdown_toolset/string_tools.py
similarity index 100%
rename from pkg/string_tools.py
rename to markdown_toolset/string_tools.py
diff --git a/markdown_toolset/transformers/__init__.py b/markdown_toolset/transformers/__init__.py
new file mode 100644
index 0000000..a0a76a1
--- /dev/null
+++ b/markdown_toolset/transformers/__init__.py
@@ -0,0 +1,8 @@
+from .md.transformer import ArticleTransformer as MarkdownArticleTransformer
+from .html.transformer import ArticleTransformer as HTMLArticleTransformer
+
+from .helpers import transform_article
+
+
+TRANSFORMERS = [MarkdownArticleTransformer, HTMLArticleTransformer]
+__all__ = [TRANSFORMERS, transform_article]
diff --git a/markdown_toolset/transformers/helpers.py b/markdown_toolset/transformers/helpers.py
new file mode 100644
index 0000000..67e8e17
--- /dev/null
+++ b/markdown_toolset/transformers/helpers.py
@@ -0,0 +1,21 @@
+from io import StringIO
+from typing import Any, List
+from markdown_toolset.image_downloader import ImageDownloader
+
+
+def transform_article(article_path: str, input_format_list: List[str], transformers_list: List[Any],
+                      img_downloader: ImageDownloader) -> str:
+    """
+    Download images and fix URL's.
+    """
+    transformers = [tr for ifmt in input_format_list
+                    for tr in transformers_list if tr is not None and tr.format == ifmt]
+
+    with open(article_path, 'r', encoding='utf8') as article_file:
+        result = StringIO(article_file.read())
+
+    for transformer in transformers:
+        lines = transformer(result, img_downloader).run()
+        result = StringIO(''.join(lines))
+
+    return result.read()
diff --git a/pkg/formatters/__init__.py b/markdown_toolset/transformers/html/__init__.py
similarity index 100%
rename from pkg/formatters/__init__.py
rename to markdown_toolset/transformers/html/__init__.py
diff --git a/pkg/transformers/html/transformer.py b/markdown_toolset/transformers/html/transformer.py
similarity index 100%
rename from pkg/transformers/html/transformer.py
rename to markdown_toolset/transformers/html/transformer.py
diff --git a/pkg/transformers/__init__.py b/markdown_toolset/transformers/md/__init__.py
similarity index 100%
rename from pkg/transformers/__init__.py
rename to markdown_toolset/transformers/md/__init__.py
diff --git a/pkg/transformers/md/transformer.py b/markdown_toolset/transformers/md/transformer.py
similarity index 100%
rename from pkg/transformers/md/transformer.py
rename to markdown_toolset/transformers/md/transformer.py
diff --git a/pkg/www_tools.py b/markdown_toolset/www_tools.py
similarity index 100%
rename from pkg/www_tools.py
rename to markdown_toolset/www_tools.py
diff --git a/pkg/transformers/md/__init__.py b/pkg/transformers/md/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9ce97ca
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,7 @@
+[build-system]
+# These are the assumed default build requirements from pip:
+# https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support
+requires = [
+    "setuptools>=43.0.0", "wheel"
+]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..69f9032
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,37 @@
+[metadata]
+name = markdown-tool
+# This includes the license file(s) in the wheel.
+# https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file
+license_files = LICENSE.txt
+author = Artiom N.
+# author_email = author@example.com
+description = Markdown articles downloader and converter
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://github.com/artiomn/markdown_articles_tool
+project_urls =
+    Bug Tracker = https://github.com/artiomn/markdown_articles_tool/issues
+    Source Code = https://github.com/artiomn/markdown_articles_tool/
+keywords = html, markdown, pdf, downloader, images, markdown-parser, markdown-to-html, markdown converter, md, markdown-to-pdf, markdown-articles
+classifiers =
+    Development Status :: 3 - Alpha
+    Intended Audience :: Developers
+    Intended Audience :: Science/Research
+    Intended Audience :: End Users/Desktop
+    Intended Audience :: Other Audience
+    Topic :: Text Processing :: Markup :: Markdown
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
+
+[options]
+package_dir =
+    = .
+packages = find:
+python_requires = >=3.8
+
+[options.packages.find]
+where = .
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..9a74caf
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,17 @@
+from setuptools import setup
+import pathlib
+
+here = pathlib.Path(__file__).parent.resolve()
+
+requirements = [r for r in (here / 'requirements.txt').read_text(encoding='utf-8').split()
+                if r and not r.lstrip().startswith('#')]
+version = here / 'markdown_toolset' / '__version__.py'
+v = compile(version.read_text(encoding='utf-8'), '', 'exec')
+exec(v)
+
+setup(
+    install_requires=requirements,
+    scripts=['markdown_tool.py'],
+    version=__version__,
+    zip_safe=False
+)


Test patch:
diff --git a/pkg/transformers/html/__init__.py b/tests/__init__.py
similarity index 100%
rename from pkg/transformers/html/__init__.py
rename to tests/__init__.py