diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..31e7403 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [master, dev] + pull_request: + workflow_dispatch: + +jobs: + run-tests: + runs-on: ${{matrix.platform}} + strategy: + matrix: + platform: [ubuntu-latest, macos-latest] # Enable `windows-latest` once someone on a windows machine can figure out why os.rename doesn't work + python-version: ["3.7", "3.8", "3.9"] + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/cache@v2 + id: cache-tox-tests + with: + path: .tox + key: ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ hashFiles('requirements.txt') }}-${{ hashFiles('tests/requirements.txt') }}-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-tox-tests + + - run: pip install tox + + - name: Run tox + run: tox + + - uses: codecov/codecov-action@v2.1.0 + if: ${{runner.os == 'Linux' && matrix.python-version == '3.9'}} + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c18ad7c --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2015 Bhrigu Srivastava +Copyright (c) 2022 Jan-Hendrik Ewers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 37f2179..c96a344 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,22 @@ # Classifier + +[![CI](https://github.com/iwishiwasaneagle/classifier/actions/workflows/CI.yml/badge.svg)](https://github.com/iwishiwasaneagle/classifier/actions/workflows/CI.yml) +[![codecov](https://codecov.io/gh/iwishiwasaneagle/classifier/branch/master/graph/badge.svg?token=ZW51EYKLL0)](https://codecov.io/gh/iwishiwasaneagle/classifier) +[![PyPI version](https://badge.fury.io/py/classifier-reborn.svg)](https://badge.fury.io/py/classifier-reborn) +[![GitHub](https://img.shields.io/github/license/iwishiwasaneagle/classifier)](https://github.com/iwishiwasaneagle/classifier/blob/master/LICENSE) + + Organize files in your current directory, by classifying them into folders of music, pdfs, images, etc. ## Installation + ```sh -$ pip install classifier +$ pip install classifier-reborn ``` + #### Compatibility -* Python 2.7 / Python 3.4 +* Python 3.7 - Python 3.9 * Linux / OSX / Windows diff --git a/classifier/__init__.py b/classifier/__init__.py index e69de29..05b266a 100644 --- a/classifier/__init__.py +++ b/classifier/__init__.py @@ -0,0 +1,2 @@ +from ._version import __version__ +from .classifier import main \ No newline at end of file diff --git a/classifier/_version.py b/classifier/_version.py new file mode 100644 index 0000000..f49313c --- /dev/null +++ b/classifier/_version.py @@ -0,0 +1,14 @@ +import sys + +if sys.version_info[:2] >= (3, 8): + from importlib.metadata import PackageNotFoundError, version # pragma: no cover +else: + from importlib_metadata import PackageNotFoundError, version # pragma: no cover + +try: + dist_name = "classifier-reborn" + __version__ = version(dist_name) +except PackageNotFoundError: # pragma: no cover + __version__ = "unknown" +finally: + del version, PackageNotFoundError diff --git a/classifier/classifier.py b/classifier/classifier.py index bd22471..5bb9a4d 100755 --- a/classifier/classifier.py +++ b/classifier/classifier.py @@ -2,12 +2,13 @@ """ Classifier ----------------Contributors---------------- - https://github.com/bhrigu123/classifier/graphs/contributors + https://github.com/iwishiwasaneagle/classifier/graphs/contributors ----------------Maintainer---------------- - Bhrigu Srivastava + Jan-Hendrik Ewers ----------------License---------------- The MIT License [https://opensource.org/licenses/MIT] Copyright (c) 2015 Bhrigu Srivastava http://bhrigu.me + Copyright (c) 2022 Jan-Hendrik Ewers https://janhendrikewers.uk """ @@ -16,9 +17,10 @@ import os import subprocess import sys +from loguru import logger +from classifier import __version__ -VERSION = 'Classifier 2.0' DIRCONFFILE = '.classifier.conf' PLATFORM = sys.platform OS = os.name @@ -47,7 +49,11 @@ class Classifier: Documents - https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions """ - def __init__(self): + def __init__(self, test=False): + if not test: + self.setup_parser() + + def setup_parser(self): self.description = "Organize files in your directory instantly,by classifying them into different folders" self.parser = argparse.ArgumentParser(description=self.description) @@ -109,7 +115,7 @@ def create_default_config(self): "DEBPackages: deb\n" + "Programs: exe, msi\n" + "RPMPackages: rpm") - print("CONFIG file created at: "+CONFIG) + logger.info("CONFIG file created at: "+CONFIG) def checkconfig(self): """ create a default config if not available """ @@ -131,7 +137,7 @@ def moveto(self, filename, from_folder, to_folder): to_file = os.path.join(to_folder, filename) # to move only files, not folders if not to_file == from_file: - print('moved: ' + str(to_file)) + logger.info('moved: ' + str(to_file)) if os.path.isfile(from_file): if not os.path.exists(to_folder): os.makedirs(to_folder) @@ -162,7 +168,7 @@ def classify(self, formats, output, directory): try: self.moveto(file, directory, folder) except Exception as e: - print('Cannot move file - {} - {}'.format(file, str(e))) + logger.info('Cannot move file - {} - {}'.format(file, str(e))) """ elif os.path.isdir(os.path.join(directory, file)) and self.args.recursive: self.classify(self.formats, output, os.path.join(directory, file)) @@ -170,11 +176,11 @@ def classify(self, formats, output, directory): return def classify_by_date(self, date_format, output, directory): - print("Scanning Files") + logger.info("Scanning Files") files = [x for x in os.listdir(directory) if not x.startswith('.')] creation_dates = map(lambda x: (x, arrow.get(os.path.getctime(os.path.join(directory, x)))), files) - print(creation_dates) + logger.info(creation_dates) for file, creation_date in creation_dates: folder = creation_date.format(date_format) @@ -197,13 +203,13 @@ def _format_arg(self, arg): def run(self): if self.args.version: # Show version information and quit - print(VERSION) + logger.info(__version__) return False if self.args.types: # Show file format information then quit for key, value in self.formats.items(): - print(key + ': '+ value) + logger.info(key + ': '+ value) return False if self.args.edittypes: @@ -220,7 +226,7 @@ def run(self): return if bool(self.args.specific_folder) ^ bool(self.args.specific_types): - print( + logger.info( 'Specific Folder and Specific Types need to be specified together') sys.exit() @@ -251,7 +257,7 @@ def run(self): if self.args.dateformat: if not self.args.date: - print( + logger.info( 'Dateformat -df must be given alongwith date -dt option') sys.exit() @@ -261,30 +267,32 @@ def run(self): else: self.classify_by_date(self.dateformat, output, directory) elif self.dirconf and os.path.isfile(self.dirconf): - print('Found config in current directory') + logger.info('Found config in current directory') if self.args.output: - print('Your output directory is being ignored!!!') + logger.info('Your output directory is being ignored!!!') for items in open(self.dirconf, "r"): # reset formats for individual folders self.formats = {} try: (key, dst, val) = items.split(':') self.formats[key] = val.replace('\n', '').split(',') - print("\nScanning: " + directory + + logger.info("\nScanning: " + directory + "\nFor: " + key + '\nFormats: ' + val) self.classify(self.formats, dst, directory) except ValueError: - print("Your local config file is malformed. Please check and try again.") + logger.info("Your local config file is malformed. Please check and try again.") return False else: - print("\nScanning Folder: " + directory) + logger.info("\nScanning Folder: " + directory) if self.args.specific_types: - print("For: " + str(self.formats.items())) + logger.info("For: " + str(self.formats.items())) else: - print("Using the default CONFIG File\n") + logger.info("Using the default CONFIG File\n") self.classify(self.formats, output, directory) - print("Done!\n") + logger.info("Done!\n") return True +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..89157ad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! +requires = ["setuptools>=46.1.0", "wheel", "setuptools_scm>=6.2"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2688e73 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +arrow==1.2.2 +loguru==0.6.0 +python-dateutil==2.8.2 +six==1.16.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0a71d1a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,73 @@ +[metadata] +name = classifier-reborn +description = Classify the files in your Downloads folder into suitable destinations +author=Bhrigu Srivastava +author_email=captain.bhrigu@gmail.com +maintainer=Jan-Hendrik Ewers +maintainer_email=dev.jh.ewers@gmail.com +license = MIT +license_files = LICENSE.txt +long_description = file: README.md +long_description_content_type = text/markdown; charset=UTF-8; variant=GFM +url = https://github.com/iwishiwasaneagle/classifier/ +project_urls = + Tracker = https://github.com/iwishiwasaneagle/classifier/issues + + +# Add here all kinds of additional classifiers as defined under +# https://pypi.org/classifiers/ +classifiers = + Development Status :: 5 - Production/Stable + License :: OSI Approved :: MIT License + Operating System :: OS Independent + + +[options] +zip_safe = False +packages = find_namespace: +include_package_data = True + +# Require a min/specific Python version (comma-separated conditions) +# python_requires = >=3.8 + +install_requires = + importlib-metadata; python_version<"3.8" + loguru==0.6.0 + arrow==1.2.2 + + +[options.packages.find] +where = ./ +exclude = + tests + +[options.extras_require] +test = + setuptools + pytest + pytest-cov + tox + +[options.entry_points] +console_scripts = + classifier = classifier:main + +[tool:pytest] +norecursedirs = + dist + build + .tox +testpaths = tests + +[flake8] +max_line_length = 88 +extend_ignore = E203, W503 +# ^ Black-compatible +# E203 and W503 have edge cases handled by black +exclude = + .tox + build + dist + .eggs + docs/conf.py +per-file-ignores = "**/__init__.py:F401" # Unused import errors \ No newline at end of file diff --git a/setup.py b/setup.py index 58d88f3..fb1c164 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,17 @@ +""" + Setup file for classifier. + Use setup.cfg to configure your project. +""" from setuptools import setup -setup( - name="classifier", - version="2.0", - description="Classify the files in your Downloads folder into suitable destinations.", - url="http://github.com/bhrigu123/classifier", - author="Bhrigu Srivastava", - author_email="captain.bhrigu@gmail.com", - license='MIT', - packages=["classifier"], - entry_points=""" - [console_scripts] - classifier = classifier.classifier:main - """, - install_requires=[ - 'arrow' - ], - zip_safe=False -) +if __name__ == "__main__": + try: + setup() + except: # noqa + print( + "\n\nAn error occurred while building the project, " + "please ensure you have the most updated version of setuptools, " + "setuptools_scm and wheel with:\n" + " pip install -U setuptools setuptools_scm wheel\n\n" + ) + raise \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..cffeec6 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +pytest +pytest-cov \ No newline at end of file diff --git a/tests/test_classifier.py b/tests/test_classifier.py index 16a5ccd..c5d51ab 100755 --- a/tests/test_classifier.py +++ b/tests/test_classifier.py @@ -1,58 +1,69 @@ # -*- coding: utf-8 -*- +from datetime import date +import pathlib import arrow import os import shutil -import unittest +import pytest import classifier.classifier as clf +import tempfile +from loguru import logger +FILES = [".test", "中文.test"] -class ClassifierTest(unittest.TestCase): - print(dir(clf)) - - __location = os.path.realpath( - os.path.join(os.getcwd(), os.path.dirname(__file__), '.unittest')) - - __tmp_files = [u'test_file', u'test_file_中文'] - __tmp_dirs = [u'test_dir', u'test_dir_中文'] - - def setUp(self): - if not os.path.exists(self.__location): - os.mkdir(self.__location) - os.chdir(self.__location) - for file_ in self.__tmp_files: - open(file_, 'w').close() - for dir_ in self.__tmp_dirs: - if not os.path.exists(dir_): - os.mkdir(dir_) - super(ClassifierTest, self).setUp() - - def tearDown(self): - shutil.rmtree(self.__location) - super(ClassifierTest, self).tearDown() - - def test_moveto(self): - target_dir = os.path.abspath(os.path.join(self.__location, 'moveto')) - for file_ in self.__tmp_files: - clf.self.moveto(file_, self.__location, target_dir) - - for file_ in self.__tmp_files: - final_file_path = os.path.join(target_dir, file_) - self.assertTrue(os.path.exists(final_file_path)) - - def test_classify_bydate(self): - date_format = 'YYYY-MM-DD' - target_files = [] - for file_ in self.__tmp_files: - target_dir = arrow.get(os.path.getctime(file_)).format(date_format) - final_file_path = os.path.join(target_dir, file_) - target_files.append(final_file_path) - clf.self.classify_by_date(date_format, self.__location) - for file_ in target_files: - self.assertTrue(os.path.exists(file_)) - for dir_ in self.__tmp_dirs: - self.assertTrue(os.path.exists(dir_)) - - -if __name__ == '__main__': - unittest.main() + +def get_temp_dir(): + return tempfile.mkdtemp() + + +def get_tmp_file(suffix=".test", dir=None): + _, path = tempfile.mkstemp(suffix=suffix, dir=dir) + return path + + +@pytest.fixture +def classifier(): + tmpdir = get_temp_dir() + tmpfiles = [get_tmp_file(f, dir=tmpdir) for f in FILES] + + os.chdir(tmpdir) + + classifier = clf.Classifier(test=True) + classifier.__location = tmpdir + classifier.__files = tmpfiles + + yield classifier + + try: + shutil.rmtree(tmpdir) + except PermissionError: + logger.error(f"Permission error when removing {tmpdir}") + + +@pytest.mark.parametrize("target", ("moveto",)) +def test_moveto(classifier, target): + target_dir = pathlib.Path(classifier.__location, target) + for file in classifier.__files: + classifier.moveto(file, classifier.__location, target_dir) + + assert target_dir.joinpath(file).is_file() + + +@pytest.mark.parametrize("target", ("bydate",)) +def test_moveto(classifier, target): + date_fmt = "YYYY-MM-DD" + target_path = pathlib.Path(classifier.__location, target) + final_path = [] + for file in classifier.__files: + final_path.append( + target_path.joinpath( + arrow.get(os.path.getctime(file)).format(date_fmt), + pathlib.Path(file).name, + ) + ) + + classifier.classify_by_date(date_fmt, target_path, classifier.__location) + + for file in final_path: + assert pathlib.Path(file).exists() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c63a288 --- /dev/null +++ b/tox.ini @@ -0,0 +1,44 @@ +[tox] +minversion = 3.15 +envlist = test +isolated_build = True + + +[testenv] +description = + test: Invoke pytest to run automated tests +setenv = + MODULE = {toxinidir} +passenv= + HOME +extras = + testing +commands = + test: pytest --cov-report=xml --cov-branch --cov classifier --cov-report term-missing {posargs} +deps = + -r {toxinidir}/requirements.txt + -r {toxinidir}/tests/requirements.txt + +[testenv:{build,clean}] +description = + build: Build the package in isolation according to PEP517, see https://github.com/pypa/build + clean: Remove old distribution files and temporary build artifacts (./build and ./dist) +skip_install = True +changedir = {toxinidir} +deps = + build: build[virtualenv] +commands = + clean: python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' + build: python -m build . + +[testenv:publish] +description = + Publish the package you have been developing to a package index server. +skip_install = True +changedir = {toxinidir} +passenv = + HOME +deps = twine +commands = + python -m twine check dist/* + python -m twine upload --config-file "{env:HOME}/.pypirc" dist/*