From e478c9efd008f5d7c6238607203b060e3e6ed2f7 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Wed, 21 Feb 2024 15:14:00 -0300 Subject: [PATCH 1/8] initial django dunder str codmeod --- .../django_model_without_dunder_str.py | 70 ++++++++++++ .../test_django_model_without_dunder_str.py | 104 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 src/core_codemods/django_model_without_dunder_str.py create mode 100644 tests/codemods/test_django_model_without_dunder_str.py diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py new file mode 100644 index 00000000..585d214d --- /dev/null +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -0,0 +1,70 @@ +from typing import Union +import libcst as cst +from codemodder.codemods.libcst_transformer import ( + LibcstResultTransformer, + LibcstTransformerPipeline, +) +from codemodder.codemods.utils_mixin import NameResolutionMixin +from core_codemods.api import ( + Metadata, + Reference, + ReviewGuidance, +) +from core_codemods.api.core_codemod import CoreCodemod + + +class DjangoModelWithoutDunderStrTransformer( + LibcstResultTransformer, NameResolutionMixin +): + change_description = "todoMoved @receiver to the top." + + def leave_ClassDef( + self, original_node: cst.ClassDef, updated_node: cst.ClassDef + ) -> Union[ + cst.BaseStatement, cst.FlattenSentinel[cst.BaseStatement], cst.RemovalSentinel + ]: + + # TODO: add filter by include or exclude that works for nodes + # that that have different start/end numbers. + if not any( + self.find_base_name(base.value) == "django.db.models.Model" + for base in original_node.bases + ): + return updated_node + + if self.implements_dunder_str(original_node): + return updated_node + + self.report_change(original_node) + dunder_str = cst.FunctionDef( + # leading_lines=[cst.EmptyLine()], + name=cst.Name("__str__"), + params=cst.Parameters(params=[cst.Param(name=cst.Name("self"))]), + body=cst.IndentedBlock(body=[cst.SimpleStatementLine(body=[cst.Pass()])]), + ) + new_body = updated_node.body.with_changes( + body=[*updated_node.body.body, dunder_str] + ) + + return updated_node.with_changes(body=new_body) + + def implements_dunder_str(self, original_node: cst.ClassDef) -> bool: + for node in original_node.body.body: + match node: + case cst.FunctionDef(name=cst.Name(value="__str__")): + return True + return False + + +DjangoModelWithoutDunderStr = CoreCodemod( + metadata=Metadata( + name="django-model-without-dunder-str", + summary="TODOEnsure Django @receiver is the first decorator", + review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW, + references=[ + Reference(url="todohttps://docs.djangoproject.com/en/4.1/topics/signals/"), + ], + ), + transformer=LibcstTransformerPipeline(DjangoModelWithoutDunderStrTransformer), + detector=None, +) diff --git a/tests/codemods/test_django_model_without_dunder_str.py b/tests/codemods/test_django_model_without_dunder_str.py new file mode 100644 index 00000000..5c72ff9d --- /dev/null +++ b/tests/codemods/test_django_model_without_dunder_str.py @@ -0,0 +1,104 @@ +from core_codemods.django_model_without_dunder_str import DjangoModelWithoutDunderStr +from tests.codemods.base_codemod_test import BaseCodemodTest + + +class TestDjangoModelWithoutDunderStr(BaseCodemodTest): + codemod = DjangoModelWithoutDunderStr + + def test_name(self): + assert self.codemod.name == "django-model-without-dunder-str" + + def test_no_change(self, tmpdir): + input_code = """ + from django.db import models + + class User(models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + + def __str__(self): + return "doesntmatter" + """ + self.run_and_assert(tmpdir, input_code, input_code) + + def test_no_dunder_str(self, tmpdir): + input_code = """ + from django.db import models + + class User(models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + + @property + def decorated_name(self): + return f"***{self.name}***" + + def something(): + pass + """ + expected = """ + from django.db import models + + class User(models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + + @property + def decorated_name(self): + return f"***{self.name}***" + def __str__(self): + pass + + def something(): + pass + """ + self.run_and_assert(tmpdir, input_code, expected) + + # def test_simple_alias(self, tmpdir): + # input_code = """ + # from django.dispatch import receiver as rec + # + # @csrf_exempt + # @rec(request_finished) + # def foo(): + # pass + # """ + # expected = """ + # from django.dispatch import receiver as rec + # + # @rec(request_finished) + # @csrf_exempt + # def foo(): + # pass + # """ + # self.run_and_assert(tmpdir, input_code, expected) + # + # def test_no_receiver(self, tmpdir): + # input_code = """ + # @csrf_exempt + # def foo(): + # pass + # """ + # self.run_and_assert(tmpdir, input_code, input_code) + # + # def test_receiver_but_not_djangos(self, tmpdir): + # input_code = """ + # from not_django import receiver + # + # @csrf_exempt + # @receiver(request_finished) + # def foo(): + # pass + # """ + # self.run_and_assert(tmpdir, input_code, input_code) + # + # def test_receiver_on_top(self, tmpdir): + # input_code = """ + # from django.dispatch import receiver + # + # @receiver(request_finished) + # @csrf_exempt + # def foo(): + # pass + # """ + # self.run_and_assert(tmpdir, input_code, input_code) From 233ada2dcc36849e43b4e3bbd9cdc6fa9dc4996e Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Wed, 21 Feb 2024 15:55:09 -0300 Subject: [PATCH 2/8] attempt to add leading line --- src/core_codemods/django_model_without_dunder_str.py | 3 ++- tests/codemods/test_django_model_without_dunder_str.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index 585d214d..d0ff0abc 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -36,8 +36,9 @@ def leave_ClassDef( return updated_node self.report_change(original_node) + dunder_str = cst.FunctionDef( - # leading_lines=[cst.EmptyLine()], + leading_lines=[cst.EmptyLine()], name=cst.Name("__str__"), params=cst.Parameters(params=[cst.Param(name=cst.Name("self"))]), body=cst.IndentedBlock(body=[cst.SimpleStatementLine(body=[cst.Pass()])]), diff --git a/tests/codemods/test_django_model_without_dunder_str.py b/tests/codemods/test_django_model_without_dunder_str.py index 5c72ff9d..4c845002 100644 --- a/tests/codemods/test_django_model_without_dunder_str.py +++ b/tests/codemods/test_django_model_without_dunder_str.py @@ -46,9 +46,10 @@ class User(models.Model): @property def decorated_name(self): return f"***{self.name}***" + def __str__(self): pass - + def something(): pass """ From 25d79a9ac88058667e8e4dbb314772bdfab3b2b1 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Thu, 22 Feb 2024 08:22:25 -0300 Subject: [PATCH 3/8] make dedent=False --- src/core_codemods/django_model_without_dunder_str.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index d0ff0abc..c65d6c5b 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -38,7 +38,7 @@ def leave_ClassDef( self.report_change(original_node) dunder_str = cst.FunctionDef( - leading_lines=[cst.EmptyLine()], + leading_lines=[cst.EmptyLine(indent=False)], name=cst.Name("__str__"), params=cst.Parameters(params=[cst.Param(name=cst.Name("self"))]), body=cst.IndentedBlock(body=[cst.SimpleStatementLine(body=[cst.Pass()])]), From 435dc457bd28ed9204894ce90de3a3e04d90ab39 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Fri, 23 Feb 2024 08:44:24 -0300 Subject: [PATCH 4/8] test django dunder str codemod correctly formats model --- .../test_django_model_without_dunder_str.py | 52 +++++++++++++++++++ .../codemods/test/integration_utils.py | 7 ++- src/codemodder/codemods/test/validations.py | 21 ++++++-- src/core_codemods/__init__.py | 3 ++ .../django_model_without_dunder_str.py | 27 +++++++--- .../test_django_model_without_dunder_str.py | 4 +- .../django-project/mysite/mysite/models.py | 13 +++++ 7 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 integration_tests/test_django_model_without_dunder_str.py create mode 100644 tests/samples/django-project/mysite/mysite/models.py diff --git a/integration_tests/test_django_model_without_dunder_str.py b/integration_tests/test_django_model_without_dunder_str.py new file mode 100644 index 00000000..b059b1a2 --- /dev/null +++ b/integration_tests/test_django_model_without_dunder_str.py @@ -0,0 +1,52 @@ +from core_codemods.django_model_without_dunder_str import ( + DjangoModelWithoutDunderStr, + DjangoModelWithoutDunderStrTransformer, +) +from integration_tests.base_test import ( + BaseIntegrationTest, + original_and_expected_from_code_path, +) + + +class TestDjangoModelWithoutDunderStr(BaseIntegrationTest): + codemod = DjangoModelWithoutDunderStr + code_path = "tests/samples/django-project/mysite/mysite/models.py" + original_code, expected_new_code = original_and_expected_from_code_path( + code_path, + [ + (15, """\n"""), + (16, """ def __str__(self):\n"""), + (17, """ model_name = self.__class__.__name__\n"""), + ( + 18, + """ fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields])\n""", + ), + (19, """ return f"{model_name}({fields_str})"\n"""), + ], + ) + + # fmt: off + expected_diff =( + """--- \n""" + """+++ \n""" + """@@ -11,3 +11,8 @@\n""" + """ content = models.CharField(max_length=200)\n""" + """ class Meta:\n""" + """ app_label = 'myapp'\n""" + """+\n""" + """+ def __str__(self):\n""" + """+ model_name = self.__class__.__name__\n""" + """+ fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields])\n""" + """+ return f"{model_name}({fields_str})"\n""" + ) + # fmt: on + + expected_line_change = "9" + change_description = DjangoModelWithoutDunderStrTransformer.change_description + num_changed_files = 1 + + def check_code_after(self): + """Executes models.py and instantiates the model to ensure expected str representation""" + module = super().check_code_after() + inst = module.Message(pk=1, author="name", content="content") + assert str(inst) == "Message(id=1, author=name, content=content)" diff --git a/src/codemodder/codemods/test/integration_utils.py b/src/codemodder/codemods/test/integration_utils.py index 9e970f96..6caa5e73 100644 --- a/src/codemodder/codemods/test/integration_utils.py +++ b/src/codemodder/codemods/test/integration_utils.py @@ -8,6 +8,7 @@ from codemodder import __version__ from codemodder import registry from .validations import execute_code +from types import ModuleType SAMPLES_DIR = "tests/samples" # Enable import of test modules from test directory @@ -139,11 +140,13 @@ def check_code_before(self): code = f.read() assert code == self.original_code - def check_code_after(self): + def check_code_after(self) -> ModuleType: with open(self.code_path, "r", encoding="utf-8") as f: new_code = f.read() assert new_code == self.expected_new_code - execute_code(path=self.code_path, allowed_exceptions=self.allowed_exceptions) + return execute_code( + path=self.code_path, allowed_exceptions=self.allowed_exceptions + ) def test_file_rewritten(self): """ diff --git a/src/codemodder/codemods/test/validations.py b/src/codemodder/codemods/test/validations.py index 915569f5..eab41c7c 100644 --- a/src/codemodder/codemods/test/validations.py +++ b/src/codemodder/codemods/test/validations.py @@ -1,5 +1,7 @@ import importlib.util import tempfile +from types import ModuleType +from typing import Optional def execute_code(*, path=None, code=None, allowed_exceptions=None): @@ -11,20 +13,29 @@ def execute_code(*, path=None, code=None, allowed_exceptions=None): ), "Must pass either path to code or code as a str." if path: - _run_code(path, allowed_exceptions) - return + return _run_code(path, allowed_exceptions) with tempfile.NamedTemporaryFile(suffix=".py", mode="w+t") as temp: temp.write(code) - _run_code(temp.name, allowed_exceptions) + return _run_code(temp.name, allowed_exceptions) -def _run_code(path, allowed_exceptions=None): - """Execute the code in `path` in its own namespace.""" +def _run_code(path, allowed_exceptions=None) -> Optional[ModuleType]: + """ + Execute the code in `path` in its own namespace. + Return loaded module for any additional testing later on. + """ allowed_exceptions = allowed_exceptions or () spec = importlib.util.spec_from_file_location("output_code", path) + if not spec: + return None + module = importlib.util.module_from_spec(spec) + if not spec.loader: + return None try: spec.loader.exec_module(module) except allowed_exceptions: pass + + return module diff --git a/src/core_codemods/__init__.py b/src/core_codemods/__init__.py index 0129aecf..9f77b5d7 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -62,6 +62,7 @@ from .lazy_logging import LazyLogging from .str_concat_in_seq_literal import StrConcatInSeqLiteral from .fix_async_task_instantiation import FixAsyncTaskInstantiation +from .django_model_without_dunder_str import DjangoModelWithoutDunderStr registry = CodemodCollection( origin="pixee", @@ -118,6 +119,7 @@ LazyLogging, StrConcatInSeqLiteral, FixAsyncTaskInstantiation, + DjangoModelWithoutDunderStr, ], ) @@ -134,3 +136,4 @@ SonarDjangoJsonResponseType, ], ) +# Import and add Codemod class to registry above. diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index c65d6c5b..983b4223 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -37,16 +37,9 @@ def leave_ClassDef( self.report_change(original_node) - dunder_str = cst.FunctionDef( - leading_lines=[cst.EmptyLine(indent=False)], - name=cst.Name("__str__"), - params=cst.Parameters(params=[cst.Param(name=cst.Name("self"))]), - body=cst.IndentedBlock(body=[cst.SimpleStatementLine(body=[cst.Pass()])]), - ) new_body = updated_node.body.with_changes( - body=[*updated_node.body.body, dunder_str] + body=[*updated_node.body.body, dunder_str_method()] ) - return updated_node.with_changes(body=new_body) def implements_dunder_str(self, original_node: cst.ClassDef) -> bool: @@ -57,6 +50,24 @@ def implements_dunder_str(self, original_node: cst.ClassDef) -> bool: return False +def dunder_str_method() -> cst.FunctionDef: + self_body = cst.IndentedBlock( + body=[ + cst.parse_statement("model_name = self.__class__.__name__"), + cst.parse_statement( + 'fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields])' + ), + cst.parse_statement('return f"{model_name}({fields_str})"'), + ] + ) + return cst.FunctionDef( + leading_lines=[cst.EmptyLine(indent=False)], + name=cst.Name("__str__"), + params=cst.Parameters(params=[cst.Param(name=cst.Name("self"))]), + body=self_body, + ) + + DjangoModelWithoutDunderStr = CoreCodemod( metadata=Metadata( name="django-model-without-dunder-str", diff --git a/tests/codemods/test_django_model_without_dunder_str.py b/tests/codemods/test_django_model_without_dunder_str.py index 4c845002..73839318 100644 --- a/tests/codemods/test_django_model_without_dunder_str.py +++ b/tests/codemods/test_django_model_without_dunder_str.py @@ -48,7 +48,9 @@ def decorated_name(self): return f"***{self.name}***" def __str__(self): - pass + model_name = self.__class__.__name__ + fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields]) + return f"{model_name}({fields_str})" def something(): pass diff --git a/tests/samples/django-project/mysite/mysite/models.py b/tests/samples/django-project/mysite/mysite/models.py new file mode 100644 index 00000000..2086168c --- /dev/null +++ b/tests/samples/django-project/mysite/mysite/models.py @@ -0,0 +1,13 @@ +import django +from django.conf import settings +from django.db import models +# required to run this module standalone for testing +settings.configure() +django.setup() + + +class Message(models.Model): + author = models.CharField(max_length=100) + content = models.CharField(max_length=200) + class Meta: + app_label = 'myapp' From e73b5923c620da228e00292ce70373bb295bd7fe Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Fri, 23 Feb 2024 12:18:26 -0300 Subject: [PATCH 5/8] django dunder str codemod can detect if parent class has a dunder str --- src/codemodder/codemods/utils_mixin.py | 10 +++ .../django_model_without_dunder_str.py | 11 +++- .../test_django_model_without_dunder_str.py | 64 +++++-------------- 3 files changed, 34 insertions(+), 51 deletions(-) diff --git a/src/codemodder/codemods/utils_mixin.py b/src/codemodder/codemods/utils_mixin.py index ee64e699..4715a1b4 100644 --- a/src/codemodder/codemods/utils_mixin.py +++ b/src/codemodder/codemods/utils_mixin.py @@ -281,6 +281,16 @@ def find_accesses(self, node) -> Collection[Access]: return scope.accesses[node] return {} + def class_has_method(self, classdef: cst.ClassDef, method_name: str) -> bool: + """Check if a given class definition implements a method of name `method_name`.""" + for node in classdef.body.body: + match node: + case cst.FunctionDef( + name=cst.Name(value=value) + ) if value == method_name: + return True + return False + class AncestorPatternsMixin(MetadataDependent): METADATA_DEPENDENCIES: ClassVar[Collection[ProviderT]] = (ParentNodeProvider,) diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index 983b4223..120bfff3 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -43,9 +43,14 @@ def leave_ClassDef( return updated_node.with_changes(body=new_body) def implements_dunder_str(self, original_node: cst.ClassDef) -> bool: - for node in original_node.body.body: - match node: - case cst.FunctionDef(name=cst.Name(value="__str__")): + """Check if a ClassDef or its bases implement `__str__`""" + if self.class_has_method(original_node, "__str__"): + return True + + for base in original_node.bases: + if maybe_assignment := self.find_single_assignment(base.value): + classdef = maybe_assignment.node + if self.class_has_method(classdef, "__str__"): return True return False diff --git a/tests/codemods/test_django_model_without_dunder_str.py b/tests/codemods/test_django_model_without_dunder_str.py index 73839318..14505491 100644 --- a/tests/codemods/test_django_model_without_dunder_str.py +++ b/tests/codemods/test_django_model_without_dunder_str.py @@ -57,51 +57,19 @@ def something(): """ self.run_and_assert(tmpdir, input_code, expected) - # def test_simple_alias(self, tmpdir): - # input_code = """ - # from django.dispatch import receiver as rec - # - # @csrf_exempt - # @rec(request_finished) - # def foo(): - # pass - # """ - # expected = """ - # from django.dispatch import receiver as rec - # - # @rec(request_finished) - # @csrf_exempt - # def foo(): - # pass - # """ - # self.run_and_assert(tmpdir, input_code, expected) - # - # def test_no_receiver(self, tmpdir): - # input_code = """ - # @csrf_exempt - # def foo(): - # pass - # """ - # self.run_and_assert(tmpdir, input_code, input_code) - # - # def test_receiver_but_not_djangos(self, tmpdir): - # input_code = """ - # from not_django import receiver - # - # @csrf_exempt - # @receiver(request_finished) - # def foo(): - # pass - # """ - # self.run_and_assert(tmpdir, input_code, input_code) - # - # def test_receiver_on_top(self, tmpdir): - # input_code = """ - # from django.dispatch import receiver - # - # @receiver(request_finished) - # @csrf_exempt - # def foo(): - # pass - # """ - # self.run_and_assert(tmpdir, input_code, input_code) + def test_model_inherits_dunder_str(self, tmpdir): + input_code = """ + from django.db import models + + class Custom: + def __str__(self): + pass + + class User(Custom, models.Model): + name = models.CharField(max_length=100) + phone = models.IntegerField(blank=True) + + def something(): + pass + """ + self.run_and_assert(tmpdir, input_code, input_code) From 93d81b9fac82ca6c11b0f3ede73b2e99ad74316e Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Sun, 25 Feb 2024 15:36:16 -0300 Subject: [PATCH 6/8] document django str dunder codemod --- src/codemodder/scripts/generate_docs.py | 4 +++ src/core_codemods/__init__.py | 1 - .../django_model_without_dunder_str.py | 10 ++++--- .../docs/pixee_python_django-debug-flag-on.md | 2 +- ..._python_django-model-without-dunder-str.md | 30 +++++++++++++++++++ ...python_django-session-cookie-secure-off.md | 2 +- 6 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md diff --git a/src/codemodder/scripts/generate_docs.py b/src/codemodder/scripts/generate_docs.py index b10fbe10..88b25670 100644 --- a/src/codemodder/scripts/generate_docs.py +++ b/src/codemodder/scripts/generate_docs.py @@ -234,6 +234,10 @@ class DocMetadata: importance="Low", guidance_explained="Manual instantiation of `asyncio.Task` is discouraged. We believe this change is safe and will not cause any issues.", ), + "django-model-without-dunder-str": DocMetadata( + importance="Low", + guidance_explained="This codemod is a great starting point for models with few fields. We encourage you to write custom `__str__` methods that best suit your Django application.", + ), } METADATA = CORE_METADATA | { diff --git a/src/core_codemods/__init__.py b/src/core_codemods/__init__.py index 9f77b5d7..86712f5f 100644 --- a/src/core_codemods/__init__.py +++ b/src/core_codemods/__init__.py @@ -136,4 +136,3 @@ SonarDjangoJsonResponseType, ], ) -# Import and add Codemod class to registry above. diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index 120bfff3..3239d0d6 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -16,7 +16,7 @@ class DjangoModelWithoutDunderStrTransformer( LibcstResultTransformer, NameResolutionMixin ): - change_description = "todoMoved @receiver to the top." + change_description = "Add `__str__` definition to `django` Model class." def leave_ClassDef( self, original_node: cst.ClassDef, updated_node: cst.ClassDef @@ -76,10 +76,12 @@ def dunder_str_method() -> cst.FunctionDef: DjangoModelWithoutDunderStr = CoreCodemod( metadata=Metadata( name="django-model-without-dunder-str", - summary="TODOEnsure Django @receiver is the first decorator", - review_guidance=ReviewGuidance.MERGE_WITHOUT_REVIEW, + summary="Ensure Django Model Classes Implement A `__str__` Method", + review_guidance=ReviewGuidance.MERGE_AFTER_REVIEW, references=[ - Reference(url="todohttps://docs.djangoproject.com/en/4.1/topics/signals/"), + Reference( + url="https://docs.djangoproject.com/en/5.0/ref/models/instances/#django.db.models.Model.__str__" + ), ], ), transformer=LibcstTransformerPipeline(DjangoModelWithoutDunderStrTransformer), diff --git a/src/core_codemods/docs/pixee_python_django-debug-flag-on.md b/src/core_codemods/docs/pixee_python_django-debug-flag-on.md index 2e00be75..80dbfb6c 100644 --- a/src/core_codemods/docs/pixee_python_django-debug-flag-on.md +++ b/src/core_codemods/docs/pixee_python_django-debug-flag-on.md @@ -1,4 +1,4 @@ -This codemod will flip django's `DEBUG` flag to `False` if it's `True` on the `settings.py` file within django's default directory structure. +This codemod will flip Django's `DEBUG` flag to `False` if it's `True` on the `settings.py` file within Django's default directory structure. Having the debug flag on may result in sensitive information exposure. When an exception occurs while the `DEBUG` flag in on, it will dump metadata of your environment, including the settings module. The attacker can purposefully request a non-existing url to trigger an exception and gather information about your system. diff --git a/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md b/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md new file mode 100644 index 00000000..4ce94f53 --- /dev/null +++ b/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md @@ -0,0 +1,30 @@ +If you've ever actively developed or debugged a Django application, you may have noticed Django models and their instances can sometimes be hard to read or distinguish one instance from another. Loading models in the interactive Django console or viewing them in the admin interface can be puzzling. This is because Django is trying to display your model objects as a plain strings. + +We've written this codemod to make your model objects human-readable. It will automatically detect all of your model's fields and display them as a nice string. + +For example, the `Question` model from Django's popular Poll App tutorial will look like this: +```diff +from django.db import models + +class Question(models.Model): + question_text = models.CharField(max_length=200) + pub_date = models.DateTimeField("date published") ++ ++ def __str__(self): ++ model_name = self.__class__.__name__ ++ fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields]) ++ return f"{model_name}({fields_str})" +``` + +Without this change, the `Question` objects look like this in the interactive Django shell: +``` +>>> Question.objects.all() +]> +``` +With this codemod's addition of `__str__`, it now looks like: +``` +>>> Question.objects.all() +]> +``` + +You'll notice this change works great for models with only a handful of fields. We encourage you to use this codemod's change as a starting point for further customization. diff --git a/src/core_codemods/docs/pixee_python_django-session-cookie-secure-off.md b/src/core_codemods/docs/pixee_python_django-session-cookie-secure-off.md index 970876c3..594de2e5 100644 --- a/src/core_codemods/docs/pixee_python_django-session-cookie-secure-off.md +++ b/src/core_codemods/docs/pixee_python_django-session-cookie-secure-off.md @@ -1,4 +1,4 @@ -This codemod will set django's `SESSION_COOKIE_SECURE` flag to `True` if it's `False` or missing on the `settings.py` file within django's default directory structure. +This codemod will set Django's `SESSION_COOKIE_SECURE` flag to `True` if it's `False` or missing on the `settings.py` file within Django's default directory structure. ```diff + SESSION_COOKIE_SECURE = True From ef37526d4a29959bc549e0a85e8569ce1a3da12f Mon Sep 17 00:00:00 2001 From: Dani Alcala <112832187+clavedeluna@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:43:56 -0300 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: Dan D'Avella --- src/core_codemods/django_model_without_dunder_str.py | 2 +- .../docs/pixee_python_django-model-without-dunder-str.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index 3239d0d6..5ce5ebc3 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -76,7 +76,7 @@ def dunder_str_method() -> cst.FunctionDef: DjangoModelWithoutDunderStr = CoreCodemod( metadata=Metadata( name="django-model-without-dunder-str", - summary="Ensure Django Model Classes Implement A `__str__` Method", + summary="Ensure Django Model Classes Implement a `__str__` Method", review_guidance=ReviewGuidance.MERGE_AFTER_REVIEW, references=[ Reference( diff --git a/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md b/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md index 4ce94f53..61952c1e 100644 --- a/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md +++ b/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md @@ -1,8 +1,8 @@ -If you've ever actively developed or debugged a Django application, you may have noticed Django models and their instances can sometimes be hard to read or distinguish one instance from another. Loading models in the interactive Django console or viewing them in the admin interface can be puzzling. This is because Django is trying to display your model objects as a plain strings. +If you've ever actively developed or debugged a Django application, you may have noticed that the string representations of Django models and their instances can sometimes be hard to read or to distinguish from one another. Loading models in the interactive Django console or viewing them in the admin interface can be puzzling. This is because the default string representation of Django models is fairly generic. -We've written this codemod to make your model objects human-readable. It will automatically detect all of your model's fields and display them as a nice string. +This codemod is intended to make the string representation of your model objects more human-readable. It will automatically detect all of your model's fields and display them as a descriptive string. -For example, the `Question` model from Django's popular Poll App tutorial will look like this: +For example, the default string representation of the `Question` model from Django's popular Poll App tutorial looks like this: ```diff from django.db import models @@ -16,7 +16,7 @@ class Question(models.Model): + return f"{model_name}({fields_str})" ``` -Without this change, the `Question` objects look like this in the interactive Django shell: +Without this change, the string representation of `Question` objects look like this in the interactive Django shell: ``` >>> Question.objects.all() ]> From f82f43d45ac92819e4b46798ae3d04758547b065 Mon Sep 17 00:00:00 2001 From: clavedeluna Date: Tue, 27 Feb 2024 17:50:49 -0300 Subject: [PATCH 8/8] change from list to gen --- integration_tests/test_django_model_without_dunder_str.py | 6 +++--- src/codemodder/codemods/test/integration_utils.py | 2 +- src/core_codemods/django_model_without_dunder_str.py | 2 +- .../docs/pixee_python_django-model-without-dunder-str.md | 2 +- tests/codemods/test_django_model_without_dunder_str.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration_tests/test_django_model_without_dunder_str.py b/integration_tests/test_django_model_without_dunder_str.py index b059b1a2..0f1b16ae 100644 --- a/integration_tests/test_django_model_without_dunder_str.py +++ b/integration_tests/test_django_model_without_dunder_str.py @@ -2,7 +2,7 @@ DjangoModelWithoutDunderStr, DjangoModelWithoutDunderStrTransformer, ) -from integration_tests.base_test import ( +from codemodder.codemods.test import ( BaseIntegrationTest, original_and_expected_from_code_path, ) @@ -19,7 +19,7 @@ class TestDjangoModelWithoutDunderStr(BaseIntegrationTest): (17, """ model_name = self.__class__.__name__\n"""), ( 18, - """ fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields])\n""", + """ fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))\n""", ), (19, """ return f"{model_name}({fields_str})"\n"""), ], @@ -36,7 +36,7 @@ class TestDjangoModelWithoutDunderStr(BaseIntegrationTest): """+\n""" """+ def __str__(self):\n""" """+ model_name = self.__class__.__name__\n""" - """+ fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields])\n""" + """+ fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))\n""" """+ return f"{model_name}({fields_str})"\n""" ) # fmt: on diff --git a/src/codemodder/codemods/test/integration_utils.py b/src/codemodder/codemods/test/integration_utils.py index 6caa5e73..284a57f9 100644 --- a/src/codemodder/codemods/test/integration_utils.py +++ b/src/codemodder/codemods/test/integration_utils.py @@ -141,7 +141,7 @@ def check_code_before(self): assert code == self.original_code def check_code_after(self) -> ModuleType: - with open(self.code_path, "r", encoding="utf-8") as f: + with open(self.code_path, "r", encoding="utf-8") as f: # type: ignore new_code = f.read() assert new_code == self.expected_new_code return execute_code( diff --git a/src/core_codemods/django_model_without_dunder_str.py b/src/core_codemods/django_model_without_dunder_str.py index 5ce5ebc3..be1da23f 100644 --- a/src/core_codemods/django_model_without_dunder_str.py +++ b/src/core_codemods/django_model_without_dunder_str.py @@ -60,7 +60,7 @@ def dunder_str_method() -> cst.FunctionDef: body=[ cst.parse_statement("model_name = self.__class__.__name__"), cst.parse_statement( - 'fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields])' + 'fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))' ), cst.parse_statement('return f"{model_name}({fields_str})"'), ] diff --git a/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md b/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md index 61952c1e..9d3ba768 100644 --- a/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md +++ b/src/core_codemods/docs/pixee_python_django-model-without-dunder-str.md @@ -12,7 +12,7 @@ class Question(models.Model): + + def __str__(self): + model_name = self.__class__.__name__ -+ fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields]) ++ fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields)) + return f"{model_name}({fields_str})" ``` diff --git a/tests/codemods/test_django_model_without_dunder_str.py b/tests/codemods/test_django_model_without_dunder_str.py index 14505491..f230e81c 100644 --- a/tests/codemods/test_django_model_without_dunder_str.py +++ b/tests/codemods/test_django_model_without_dunder_str.py @@ -1,5 +1,5 @@ from core_codemods.django_model_without_dunder_str import DjangoModelWithoutDunderStr -from tests.codemods.base_codemod_test import BaseCodemodTest +from codemodder.codemods.test import BaseCodemodTest class TestDjangoModelWithoutDunderStr(BaseCodemodTest): @@ -49,7 +49,7 @@ def decorated_name(self): def __str__(self): model_name = self.__class__.__name__ - fields_str = ", ".join([f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields]) + fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields)) return f"{model_name}({fields_str})" def something():