From cbc559fe2ce65f1101439431d313556c6eb2c4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Wed, 4 Mar 2026 12:24:51 +0100 Subject: [PATCH 1/5] feat: Python 3.14 support --- .github/workflows/build.yml | 6 +++--- .github/workflows/pull_request.yml | 6 +++--- .pylintrc | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .pylintrc diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc32fa5..c7f5a1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,14 +11,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.16", "3.17", "3.18", "3.19", "3.20"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Run tests diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a4092e3..b16def0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,15 +8,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Run tests diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..ef15790 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,5 @@ +[MASTER] +ignore=.github + +[MESSAGES CONTROL] +disable = dangerous-default-value,unnecessary-lambda,broad-except,line-too-long,invalid-name,no-name-in-module,missing-module-docstring,missing-class-docstring,missing-function-docstring,eval-used,bare-except,broad-except,too-few-public-methods,import-outside-toplevel,wildcard-import,unused-wildcard-import,pointless-string-statement From 4def32c17cee40f1e2fab1463e1b0a078c4df546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Wed, 4 Mar 2026 13:03:51 +0100 Subject: [PATCH 2/5] wip --- .github/workflows/build.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7f5a1f..321fb97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.16", "3.17", "3.18", "3.19", "3.20"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.16", "3.17", "3.18", "3.19", "3.20"] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b16def0..23bbb71 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout From 77f9026a681c9ab18be92a5e5cd390951f12151e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Wed, 4 Mar 2026 13:06:25 +0100 Subject: [PATCH 3/5] remove sonar --- .github/workflows/build.yml | 17 ----------------- .github/workflows/pull_request.yml | 20 -------------------- 2 files changed, 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 321fb97..ae03e5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,20 +25,3 @@ jobs: run: | python -m pip install --upgrade pip sh test.sh - - name: SonarCloud Scan - if: ${{ matrix.python-version == '3.10'}} - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.SONAR_GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }} - with: - args: > - -Dsonar.organization=rogervila-github - -Dsonar.projectKey=rogervila_python_carbon - -Dsonar.python.coverage.reportPaths=coverage.xml - -Dsonar.python.version=3.10 - -Dsonar.sources=. - -Dsonar.exclusions=tests/** - -Dsonar.test.exclusions=tests/** - -Dsonar.tests=tests/ - -Dsonar.verbose=true diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 23bbb71..9411df9 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,23 +23,3 @@ jobs: run: | python -m pip install --upgrade pip sh test.sh - - name: Get PR author - id: get_author - run: echo ::set-output name=AUTHOR::${{ github.event.pull_request.user.login }} - - name: SonarCloud Scan - if: ${{ matrix.python-version == '3.10' && steps.get_author.outputs.AUTHOR == 'rogervila' }} - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.SONAR_GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }} - with: - args: > - -Dsonar.organization=rogervila-github - -Dsonar.projectKey=rogervila_python_carbon - -Dsonar.python.version=3.10 - -Dsonar.python.coverage.reportPaths=coverage.xml - -Dsonar.sources=. - -Dsonar.exclusions=tests/** - -Dsonar.test.exclusions=tests/** - -Dsonar.tests=tests/ - -Dsonar.verbose=true From 9f5dcd63d3dcf259a9a00668121ee7f4442309cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Wed, 4 Mar 2026 13:10:28 +0100 Subject: [PATCH 4/5] wip --- README.md | 6 ++---- setup.py | 5 +++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1b5888e..0c2b781 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,8 @@

rogervila/python_carbon

-[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=rogervila_python_carbon&metric=coverage)](https://sonarcloud.io/dashboard?id=rogervila_python_carbon) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rogervila_python_carbon&metric=alert_status)](https://sonarcloud.io/dashboard?id=rogervila_python_carbon) -[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=rogervila_python_carbon&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=rogervila_python_carbon) - +[![PyPI version](https://badge.fury.io/py/python-carbon.svg)](https://badge.fury.io/py/python-carbon) +![PyPI - Downloads](https://img.shields.io/pypi/dm/python-carbon) PHP Carbon library adapted for python. diff --git a/setup.py b/setup.py index ee73f20..6f6dbbd 100644 --- a/setup.py +++ b/setup.py @@ -25,5 +25,10 @@ 'Topic :: Software Development :: Build Tools', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', ], ) From 3545d5114bf3b1a17c17706065648f851a838924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Vil=C3=A0?= Date: Wed, 4 Mar 2026 13:18:44 +0100 Subject: [PATCH 5/5] better tests --- .gitignore | 1 + LICENSE.txt | 2 +- test.sh | 6 +- tests/test_python_carbon.py | 324 +++++++++++++----------------------- 4 files changed, 123 insertions(+), 210 deletions(-) diff --git a/.gitignore b/.gitignore index c5b8004..aeba46a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ htmlcov/ *.dat MANIFEST +.vscode/ diff --git a/LICENSE.txt b/LICENSE.txt index 2266208..ce8e6b8 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Roger Vilà +Copyright (c) 2026 Roger Vilà Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/test.sh b/test.sh index 2542634..a10e09b 100644 --- a/test.sh +++ b/test.sh @@ -1,9 +1,9 @@ #!/usr/bin/env sh pip install --user coverage -U -pip install --user python-dateutil>=2 -U +pip install --user "python-dateutil>=2" -U -coverage run -m unittest discover || exit 1 -coverage xml -i || exit 1 +python -m coverage run -m unittest discover || exit 1 +python -m coverage xml -i || exit 1 [ -f coverage.xml ] || exit 1 diff --git a/tests/test_python_carbon.py b/tests/test_python_carbon.py index 693763c..f818bff 100644 --- a/tests/test_python_carbon.py +++ b/tests/test_python_carbon.py @@ -1,225 +1,137 @@ import unittest +from datetime import timedelta from python_carbon import Carbon class test_python_carbon(unittest.TestCase): - def test_todo(self): - self.assertTrue(True) - - print('\n----\nTEST\n----\n') - + def test_carbon_methods(self) -> None: dt = Carbon.parse('2021-08-16 01:02:03') dt2 = Carbon.parse('2021-08-16 01:02:03') - print( - dt.equalTo(dt2) - ) + self.assertTrue(dt.equalTo(dt2)) dt = Carbon.now() + self.assertIsInstance(dt, Carbon) + dt = Carbon() # equivalent to Carbon.now() + self.assertIsInstance(dt, Carbon) dt = Carbon.createFromFormat('%Y-%m-%d', '2021-08-18') - - print( - dt.getTimestamp() - ) - - print( - dt.weekday() - ) - - print( - dt.format('%Y-%m-%d') - ) - - print( - dt.strftime('%Y-%m') - ) - - print( - dt.toDateTimeString() - ) - - print( - dt.addDays(1).setYear(2001).toDateTimeString() - ) - - print( - dt.year - ) - - print( - dt.getYear() - ) - - print( - dt.micro - ) - - print( - dt.timestamp - ) + self.assertEqual(dt.format('%Y-%m-%d'), '2021-08-18') + + self.assertEqual(dt.weekday(), Carbon.WEDNESDAY) + self.assertEqual(dt.format('%Y-%m-%d'), '2021-08-18') + self.assertEqual(dt.strftime('%Y-%m'), '2021-08') + self.assertEqual(dt.toDateTimeString(), '2021-08-18 00:00:00') + self.assertEqual(dt.addDays(1).setYear(2001).toDateTimeString(), '2001-08-19 00:00:00') + self.assertEqual(dt.year, 2021) + self.assertEqual(dt.getYear(), 2021) + self.assertEqual(dt.micro, 0) + self.assertIsInstance(dt.timestamp, float) dt = Carbon.now() - print( - dt.startOfWeek().format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.endOfWeek().format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.startOfDay().format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.endOfDay().format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.startOf('minute').format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.endOf('minute').format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.startOfHour().format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - dt.endOfHour().format('%Y-%m-%d %H:%M:%S.%f') - ) - - print( - Carbon.parse('2021-08-31').isLastDayOfMonth() - ) - - print( - Carbon.now().addYears(10).toDateTimeString() - ) - - print( - Carbon.now().add(10, 'years').toDateTimeString() - ) - - print( - Carbon.now().subYears(10).toDateTimeString() - ) - - print( - Carbon.now().sub(10, 'years').toDateTimeString() - ) - - print(type(Carbon.timedelta(days=1))) - - # print(dt.microseconds) - # print(dt.micro) - - print( - Carbon.now().difference(Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('years', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('months', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('weeks', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('days', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('hours', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('minutes', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('seconds', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.now().diffIn('microseconds', Carbon.parse('1992-04-17 17:00:00')) - ) - - print( - Carbon.utcnow().toCookieString() - ) - - print( - Carbon.now().toISOString() - ) - - print( - Carbon.now().getDayOfYear() - ) - - print( - Carbon.parse('2022-01-02').getDayOfYear() - ) - - print( - Carbon.parse('2022-01-01').getWeekOfYear() - ) - - print( - Carbon.now().getWeekOfMonth() - ) - - print( - Carbon.now().startOfMonth().toDateTimeString(with_milliseconds=True) - ) - - print( - Carbon.now().endOfMonth().toDateTimeString(with_milliseconds=True) - ) - - print( - Carbon.now().startOfYear().toDateTimeString(with_milliseconds=True) - ) - - print( - Carbon.now().endOfYear().toDateTimeString(with_milliseconds=True) - ) - - print( - Carbon.now().getQuarters() - ) - - print( - Carbon.now().getQuarter() - ) - - print( - Carbon.now().betweenIncluded(Carbon.yesterday(), Carbon.tomorrow()) - ) - - print( - Carbon.now().betweenExcluded(Carbon.yesterday(), Carbon.tomorrow()) - ) - - print( - Carbon.yesterday().betweenIncluded(Carbon.yesterday(), Carbon.tomorrow()) - ) - - print( - Carbon.yesterday().betweenExcluded(Carbon.yesterday(), Carbon.tomorrow()) - ) - - print('\n---\nEND\n---\n') + start_of_week = dt.startOfWeek() + end_of_week = dt.endOfWeek() + start_of_day = dt.startOfDay() + end_of_day = dt.endOfDay() + start_of_minute = dt.startOf('minute') + end_of_minute = dt.endOf('minute') + start_of_hour = dt.startOfHour() + end_of_hour = dt.endOfHour() + + self.assertEqual(start_of_week.weekday(), Carbon.MONDAY) + self.assertTrue(start_of_week.isStartOfDay()) + self.assertEqual(end_of_week.weekday(), Carbon.SUNDAY) + self.assertTrue(end_of_week.isEndOfDay()) + + self.assertTrue(start_of_day.isStartOfDay()) + self.assertTrue(end_of_day.isEndOfDay()) + + self.assertEqual(start_of_minute.second, 0) + self.assertEqual(start_of_minute.microsecond, 0) + self.assertEqual(end_of_minute.second, 59) + self.assertEqual(end_of_minute.microsecond, 999999) + + self.assertEqual(start_of_hour.minute, 0) + self.assertEqual(start_of_hour.second, 0) + self.assertEqual(start_of_hour.microsecond, 0) + self.assertEqual(end_of_hour.minute, 59) + self.assertEqual(end_of_hour.second, 59) + self.assertEqual(end_of_hour.microsecond, 999999) + + self.assertTrue(Carbon.parse('2021-08-31').isLastDayOfMonth()) + + anchor = Carbon.now() + self.assertTrue(anchor.addYears(10).equalTo(anchor.add(10, 'years'))) + self.assertTrue(anchor.subYears(10).equalTo(anchor.sub(10, 'years'))) + + self.assertIsInstance(Carbon.timedelta(days=1), timedelta) + + reference = Carbon.parse('1992-04-17 17:00:00') + now = Carbon.now() + + diff = now.difference(reference) + self.assertIsInstance(diff, dict) + self.assertEqual( + set(diff.keys()), + {'years', 'months', 'days', 'leapdays', 'hours', 'minutes', 'seconds', 'microseconds'} + ) + + diff_years = now.diffIn('years', reference) + diff_months = now.diffIn('months', reference) + diff_weeks = now.diffIn('weeks', reference) + diff_days = now.diffIn('days', reference) + diff_hours = now.diffIn('hours', reference) + diff_minutes = now.diffIn('minutes', reference) + diff_seconds = now.diffIn('seconds', reference) + diff_microseconds = now.diffIn('microseconds', reference) + + self.assertGreater(diff_years, 0) + self.assertEqual(diff_months, now.difference(reference)['months'] + diff_years * 12) + self.assertEqual(diff_weeks, diff_days / 7) + self.assertEqual(diff_hours, diff_days * 24) + self.assertEqual(diff_minutes, diff_hours * 60) + self.assertEqual(diff_seconds, diff_minutes * 60) + self.assertEqual(diff_microseconds, diff_seconds * 1000) + + cookie_string = Carbon.utcnow().toCookieString() + self.assertIsInstance(cookie_string, str) + self.assertIn('UTC', cookie_string) + + iso_string = Carbon.now().toISOString() + self.assertRegex(iso_string, r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$') + + self.assertIn(Carbon.now().getDayOfYear(), range(1, 367)) + self.assertEqual(Carbon.parse('2022-01-02').getDayOfYear(), 2) + self.assertEqual(Carbon.parse('2022-01-01').getWeekOfYear(), 0) + + week_of_month = Carbon.now().getWeekOfMonth() + self.assertIn(week_of_month, range(0, 7)) + + start_of_month = Carbon.now().startOfMonth() + end_of_month = Carbon.now().endOfMonth() + start_of_year = Carbon.now().startOfYear() + end_of_year = Carbon.now().endOfYear() + + self.assertRegex(start_of_month.toDateTimeString(with_milliseconds=True), r'^\d{4}-\d{2}-01 00:00:00\.\d{6}$') + self.assertRegex(end_of_month.toDateTimeString(with_milliseconds=True), r'^\d{4}-\d{2}-\d{2} 23:59:59\.\d{6}$') + self.assertRegex(start_of_year.toDateTimeString(with_milliseconds=True), r'^\d{4}-01-01 00:00:00\.\d{6}$') + self.assertRegex(end_of_year.toDateTimeString(with_milliseconds=True), r'^\d{4}-12-31 23:59:59\.\d{6}$') + + quarters = Carbon.now().getQuarters() + quarter = Carbon.now().getQuarter() + self.assertEqual(len(quarters), 4) + self.assertTrue(all(len(q) == 3 for q in quarters)) + self.assertIn(quarter, range(0, 4)) + + current = Carbon.now() + yesterday = Carbon.yesterday() + tomorrow = Carbon.tomorrow() + self.assertTrue(current.betweenIncluded(yesterday, tomorrow)) + self.assertTrue(current.betweenExcluded(yesterday, tomorrow)) + self.assertTrue(yesterday.betweenIncluded(yesterday, tomorrow)) + self.assertFalse(yesterday.betweenExcluded(yesterday, tomorrow)) if __name__ == '__main__':