diff --git a/.dockerignore b/.dockerignore index aad30826..647c2035 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,4 +19,6 @@ venv/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -*.code-workspace \ No newline at end of file +*.code-workspace + +venv/ \ No newline at end of file diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..a6a9434a --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,10 @@ +name: Ruff +on: [push, pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + with: + src: './src' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3c351003..e095f6e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,10 @@ FROM python:3.12-alpine # Install MySQL and PostgreSQL client libraries RUN apk update && apk add --no-cache \ mariadb-connector-c-dev \ - postgresql-dev python3-dev musl-dev + postgresql-dev python3-dev musl-dev git # Install Tox RUN pip install tox -RUN tox -e dev # Set the working directory WORKDIR /app @@ -16,5 +15,7 @@ WORKDIR /app # Copy the project files to the working directory COPY . /app +RUN tox -e dev + # Set the entrypoint command CMD ["tox"] diff --git a/docs/source/conf.py b/docs/source/conf.py index 42f5c9c6..aca97b66 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,6 @@ autoclass_content = "both" -import cities_light # -- Project information ----------------------------------------------------- diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..72fef103 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,77 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py39" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" \ No newline at end of file diff --git a/src/cities_light/__init__.py b/src/cities_light/__init__.py index 8cb355d2..668b94e0 100644 --- a/src/cities_light/__init__.py +++ b/src/cities_light/__init__.py @@ -1,6 +1,15 @@ -from .signals import * -from .exceptions import * -from .settings import * +from .signals import country_items_pre_import, country_items_post_import, \ + region_items_pre_import, region_items_post_import, \ + subregion_items_pre_import, subregion_items_post_import, \ + city_items_pre_import, city_items_post_import, \ + translation_items_pre_import # noqa: F401 +from .exceptions import CitiesLightException, InvalidItems, SourceFileDoesNotExist # noqa: F401 +from .settings import FIXTURES_BASE_URL, COUNTRY_SOURCES, REGION_SOURCES, \ + SUBREGION_SOURCES, CITY_SOURCES, TRANSLATION_LANGUAGES, \ + TRANSLATION_SOURCES, SOURCES, DATA_DIR, INDEX_SEARCH_NAMES, \ + INCLUDE_COUNTRIES, INCLUDE_CITY_TYPES, DEFAULT_APP_NAME, \ + CITIES_LIGHT_APP_NAME, ICountry, IRegion, ISubRegion, ICity, \ + IAlternate # noqa: F401 from . import version __version__ = version.version diff --git a/src/cities_light/geonames.py b/src/cities_light/geonames.py index 929c83bd..cc2add85 100644 --- a/src/cities_light/geonames.py +++ b/src/cities_light/geonames.py @@ -2,7 +2,7 @@ import zipfile import logging -from .settings import * +from .settings import DATA_DIR from .downloader import Downloader diff --git a/src/cities_light/management/commands/cities_light.py b/src/cities_light/management/commands/cities_light.py index e9907404..193225ca 100644 --- a/src/cities_light/management/commands/cities_light.py +++ b/src/cities_light/management/commands/cities_light.py @@ -6,13 +6,8 @@ from argparse import RawTextHelpFormatter import sys -if sys.platform != 'win32': - import resource - -try: - import cPickle as pickle -except ImportError: - import pickle +import resource +import pickle from django.conf import settings from django.db import transaction, connection @@ -22,9 +17,19 @@ import progressbar -from ...exceptions import * -from ...signals import * -from ...settings import * +from ...settings import ( + COUNTRY_SOURCES, REGION_SOURCES, SUBREGION_SOURCES, CITY_SOURCES, + TRANSLATION_SOURCES, DATA_DIR, TRANSLATION_LANGUAGES, + ICountry, IRegion, ISubRegion, ICity, IAlternate +) +from ...signals import ( + country_items_pre_import, region_items_pre_import, + subregion_items_pre_import, city_items_pre_import, + translation_items_pre_import, country_items_post_import, + region_items_post_import, subregion_items_post_import, + city_items_post_import +) +from ...exceptions import InvalidItems from ...geonames import Geonames from ...loading import get_cities_models from ...validators import timezone_validator diff --git a/src/cities_light/migrations/0002_city.py b/src/cities_light/migrations/0002_city.py index 5493645a..0aabf3fd 100644 --- a/src/cities_light/migrations/0002_city.py +++ b/src/cities_light/migrations/0002_city.py @@ -1,6 +1,5 @@ from django.db import models, migrations import autoslug.fields -import cities_light.models from cities_light.settings import INDEX_SEARCH_NAMES diff --git a/src/cities_light/migrations/0003_auto_20141120_0342.py b/src/cities_light/migrations/0003_auto_20141120_0342.py index a3ab75fb..0a216858 100644 --- a/src/cities_light/migrations/0003_auto_20141120_0342.py +++ b/src/cities_light/migrations/0003_auto_20141120_0342.py @@ -1,5 +1,5 @@ -from django.db import models, migrations -import cities_light.models +from django.db import migrations +from cities_light.abstract_models import ToSearchTextField from cities_light.settings import INDEX_SEARCH_NAMES @@ -14,7 +14,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='city', name='search_names', - field=cities_light.models.ToSearchTextField(default=b'', max_length=4000, db_index=INDEX_SEARCH_NAMES, blank=True), + field=ToSearchTextField(default=b'', max_length=4000, db_index=INDEX_SEARCH_NAMES, blank=True), preserve_default=True, ), ] diff --git a/src/cities_light/migrations/0004_autoslug_update.py b/src/cities_light/migrations/0004_autoslug_update.py index 8b24a3bd..06071a68 100644 --- a/src/cities_light/migrations/0004_autoslug_update.py +++ b/src/cities_light/migrations/0004_autoslug_update.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations import autoslug.fields diff --git a/src/cities_light/migrations/0006_compensate_for_0003_bytestring_bug.py b/src/cities_light/migrations/0006_compensate_for_0003_bytestring_bug.py index 085a7809..913f8673 100644 --- a/src/cities_light/migrations/0006_compensate_for_0003_bytestring_bug.py +++ b/src/cities_light/migrations/0006_compensate_for_0003_bytestring_bug.py @@ -1,4 +1,4 @@ -from django.db import migrations, models +from django.db import migrations import cities_light.abstract_models from cities_light.settings import INDEX_SEARCH_NAMES diff --git a/src/cities_light/models.py b/src/cities_light/models.py index 710695c5..fa0b4dbb 100644 --- a/src/cities_light/models.py +++ b/src/cities_light/models.py @@ -83,17 +83,12 @@ def set_city_fields(sender, instance, items, **kwargs): from .abstract_models import (AbstractCountry, AbstractRegion, AbstractSubRegion, AbstractCity, CONTINENT_CHOICES, - ToSearchTextField, to_search, to_ascii) + to_search, to_ascii) -from .signals import * -from .receivers import * -from .settings import * +from .receivers import connect_default_signals +from .settings import CITIES_LIGHT_APP_NAME, DEFAULT_APP_NAME -__all__ = ['CONTINENT_CHOICES', 'to_search', 'to_ascii', 'filter_non_cities', - 'filter_non_included_countries_country', - 'filter_non_included_countries_region', - 'filter_non_included_countries_subregion', - 'filter_non_included_countries_city'] +__all__ = ['CONTINENT_CHOICES', 'to_search', 'to_ascii'] if CITIES_LIGHT_APP_NAME == DEFAULT_APP_NAME: class Country(AbstractCountry): diff --git a/src/cities_light/receivers.py b/src/cities_light/receivers.py index 095933d7..12df635a 100644 --- a/src/cities_light/receivers.py +++ b/src/cities_light/receivers.py @@ -1,8 +1,11 @@ from django.db.models import signals from .abstract_models import to_ascii, to_search -from .settings import * -from .signals import * -from .exceptions import * +from .settings import INCLUDE_CITY_TYPES, INCLUDE_COUNTRIES +from .signals import ( + city_items_pre_import, country_items_pre_import, region_items_pre_import, + subregion_items_pre_import +) +from .exceptions import InvalidItems def set_name_ascii(sender, instance=None, **kwargs): diff --git a/src/cities_light/tests/test_downloader.py b/src/cities_light/tests/test_downloader.py index 9524afd1..238c042e 100644 --- a/src/cities_light/tests/test_downloader.py +++ b/src/cities_light/tests/test_downloader.py @@ -9,7 +9,6 @@ from cities_light.downloader import Downloader from cities_light.exceptions import SourceFileDoesNotExist -import builtins as b # do not remove class TestDownloader(test.TransactionTestCase): @@ -146,7 +145,7 @@ def test_download_calls_source_matches_destination(self, m_check): m_check.return_value = True downloader = Downloader() source = 'file:///a.txt' - destination = '/a.txt' + destination = '/tmp/a.txt' # The downloader.download will return false # as source and destination are same # The downloader.source_matches_destination will return @@ -168,7 +167,7 @@ def test_download_calls_needs_downloading(self, m_check, m_need): m_need.return_value = False downloader = Downloader() source = 'file:///a.txt' - destination = '/a.txt' + destination = '/tmp/a.txt' # Here dowaloder.needs_downloading() will return false # as the time of modifiaction of dest>= time of source # and the size od source and destination are same @@ -191,25 +190,23 @@ def test_download(self, m_check, m_need): m_need.return_value = True downloader = Downloader() source = 'file:///b.txt' - destination = '/a.txt' + destination = '/tmp/a.txt' tmpfile = tempfile.SpooledTemporaryFile(max_size=1024000, mode='wb') tmpfile.write(b'source content') tmpfile.seek(0) + mock_open = mock.mock_open() with mock.patch('cities_light.downloader.urlopen', - return_value=tmpfile): - module_name = '{}.b.open'.format(__name__) - mock_open = mock.mock_open() - # The downoader.needs_downloading will return true and last three - # lines of downloader.download will copy the source to sestination - with mock.patch(module_name, mock_open): - self.assertTrue(downloader.download( - source, - destination, - False)) - handle = mock_open() - handle.write.assert_called_once_with(b'source content') + return_value=tmpfile), mock.patch('cities_light.downloader.open', mock_open): + # The downloader.needs_downloading will return true and last three + # lines of downloader.download will copy the source to destination + self.assertTrue(downloader.download( + source, + destination, + False)) + handle = mock_open() + handle.write.assert_called_once_with(b'source content') def test_not_download(self): """Tests actual not download.""" @@ -217,7 +214,7 @@ def test_not_download(self): m.return_value = True downloader = Downloader() source = 'file:///b.txt' - destination = '/a.txt' + destination = '/tmp/a.txt' with mock.patch('cities_light.downloader.urlopen') as uo_mock: downloader.download(source, destination) uo_mock.assert_not_called() diff --git a/src/cities_light/tests/test_migrations.py b/src/cities_light/tests/test_migrations.py index 848c503c..ef920a1f 100644 --- a/src/cities_light/tests/test_migrations.py +++ b/src/cities_light/tests/test_migrations.py @@ -1,5 +1,4 @@ import unittest - from django import test from django.apps import apps from django.db.migrations.autodetector import MigrationAutodetector @@ -7,13 +6,16 @@ from django.db.migrations.questioner import ( InteractiveMigrationQuestioner, ) from django.db.migrations.state import ProjectState +import logging +logger = logging.getLogger(__name__) class TestNoMigrationLeft(test.TestCase): @unittest.skip("TODO: make the test pass") def test_no_migration_left(self): loader = MigrationLoader(None, ignore_no_migrations=True) conflicts = loader.detect_conflicts() + logger.error(conflicts) app_labels = ['cities_light'] autodetector = MigrationAutodetector( diff --git a/test_project/manage.py b/test_project/manage.py index 7646e463..7ff0e96b 100755 --- a/test_project/manage.py +++ b/test_project/manage.py @@ -7,16 +7,9 @@ try: from django.core.management import execute_from_command_line except ImportError: - # The above import may fail for some other reason. Ensure that the - # issue is really that Django is missing to avoid masking other - # exceptions on Python 2. - try: - import django - except ImportError: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) - raise + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) execute_from_command_line(sys.argv)