Compare commits

...

1088 Commits

Author SHA1 Message Date
Bastien Gerard
2f4b2aadb4 fix yaml float - string conversion 2021-10-25 14:45:07 +02:00
Bastien Gerard
f4a87bff1d remove travis deprecated config 2021-10-25 10:23:33 +02:00
Bastien Gerard
3a32c195d5 remove useless quotes in yaml 2021-10-25 10:22:27 +02:00
Bastien Gérard
880a8efc63
Merge pull request #2580 from rooterkyberian/py310
test against python 3.10
2021-10-25 10:03:23 +02:00
Maciej Urbanski
9a414edb25 unfreeze pytest (pytest>=6.2.5 is required for py3.10) 2021-10-24 18:26:56 +02:00
Maciej Urbanski
4b643ad01f test against python 3.10 2021-10-24 18:11:29 +02:00
Bastien Gérard
811dc12db5
Merge pull request #2573 from bagerard/improve_compound_indexes
Improve compound indexes in rst
2021-10-13 09:28:15 +02:00
Bastien Gérard
dbd72282bc
Merge pull request #2574 from bagerard/update_changelog_recent_merge
update changelog with recent changes that were merged
2021-10-13 09:27:52 +02:00
Bastien Gerard
686b42c29b update changelog with recent changes that were merged 2021-10-12 21:06:11 +02:00
Bastien Gerard
4210ea06c3 Improve compound indexes in rst 2021-10-12 21:02:12 +02:00
Bastien Gérard
8c3e2b340b
Merge pull request #2568 from idoshr/regex_query
Regex and whole word text search query
2021-10-12 20:47:36 +02:00
Ido Shraga
7a0a58c163 black reformat 2021-10-01 07:48:01 +03:00
Bastien Gérard
0af1a1151f
Merge pull request #2571 from bagerard/idoshr-master-fixed
Idoshr master fixed
2021-09-30 20:49:33 +02:00
Bastien Gerard
28226f81a8 black it 2021-09-30 20:39:44 +02:00
Bastien Gerard
558e3299fe Merge branch 'master' of https://github.com/idoshr/mongoengine into idoshr-master 2021-09-30 20:38:32 +02:00
Bastien Gerard
cb0035f87a minor typo fix in docstring 2021-09-30 20:25:26 +02:00
idoshr
85b0c1c945
Update AUTHORS 2021-09-20 22:36:13 +03:00
Ido Shraga
3d6b650592 add doc
change has_word ==> wholeword
2021-09-12 18:21:01 +03:00
Ido Shraga
6bc1b83695 add option to filter by free regex
and option to search by whole word
2021-09-10 16:28:06 +03:00
Ido Shraga
7e10bb2894 rename tester 2021-09-01 10:24:26 +03:00
Ido Shraga
8428da368d add update operator to documentation 2021-08-31 16:49:33 +03:00
erdenezul
5fe9436ea7
Merge pull request #2561 from bagerard/improve_index_doc
improve index doc
2021-08-23 09:22:17 +02:00
Bastien Gerard
1b249e336e improve list_indexes, ensure_indexes docstring and add index migration example 2021-08-22 22:01:05 +02:00
Bastien Gérard
533700583d
Merge pull request #2557 from yugyesh/patch-1
Minor typo error
2021-08-16 08:00:04 +02:00
Yugesh kc
639124c311
Minor typo error
Remove unwanted and
2021-08-16 11:52:33 +09:00
Bastien Gérard
8b5cf9e2be
Merge pull request #2549 from bagerard/fix_decimal_field_precision0
Fix decimal field precision0
2021-08-07 22:50:46 +02:00
Bastien Gerard
0af96b1323 Add test for DecimalField(precision=0) and raise exception if negative/invalid values are provided 2021-08-07 22:38:27 +02:00
Bastien Gerard
f6d864b6d1 Merge branch 'patch-1' of https://github.com/oleksandr-l5/mongoengine 2021-08-07 22:09:04 +02:00
Bastien Gérard
bb9ba73a7b
Merge pull request #2545 from bagerard/fix_embedded_instance_deepcopy
Fix embedded instance deepcopy
2021-08-07 21:56:55 +02:00
Bastien Gerard
66978aea67 fix test and changelog 2021-08-02 22:09:21 +02:00
Bastien Gerard
5a41998eda Updated recent bugfix with embedded DOc deepcopy so that it set _instance to None 2021-08-02 21:51:15 +02:00
Bastien Gerard
c32b308730 minor renaming in test classes 2021-08-01 14:12:27 +02:00
Timothé Perez
88642eb021 Fix deepcopy on EmbeddedDocument 2021-08-01 10:09:24 +02:00
Bastien Gérard
3b10236b5e
Merge pull request #2536 from ottj3/patch-1
Mention DynamicEmbeddedDocument in usage docs.
2021-07-02 09:51:31 +02:00
Jan-Lucas Ott
bffe058726
Mention DynamicEmbeddedDocument in embedded section. 2021-06-30 17:16:07 -04:00
Oleksandr
09c415a416
fix DecimalField precision
fix decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>] error if DecimalField param precision is zero or less
2021-06-29 17:32:51 +03:00
Bastien Gérard
4670508a1c
Merge pull request #2530 from bagerard/fix_doc_formatting
fix doc formatting (enumfield, etc)
2021-06-13 23:06:13 +02:00
Bastien Gerard
e7f2d77a75 fix doc formatting (enumfield, etc) 2021-06-13 23:01:20 +02:00
Bastien Gérard
7bf7153e4f
Merge pull request #2515 from par-tec/master
define a base exception for engine
2021-06-06 15:58:26 +02:00
Bastien Gérard
0df0585d07
Merge pull request #2528 from bagerard/precommit_update
precommit update
2021-06-05 21:29:59 +02:00
Bastien Gerard
6ae722d04b precommit update 2021-06-05 21:17:31 +02:00
Bastien Gérard
cc85165b0c
Merge pull request #2519 from bagerard/bugfix_qs_property
Fix instance._qs that was failing due to bad parametrization that went unnoticed
2021-05-18 09:48:33 +02:00
Bastien Gerard
2fd4169656 Fix instance._qs that was failing due to bad parametrization that went unnoticed 2021-05-17 22:37:35 +02:00
Daniele Marcocci
5897b1d042 blacked 2021-05-05 18:31:15 +02:00
Daniele Marcocci
756bffe868 define a base exception for engine 2021-05-05 14:59:38 +02:00
Bastien Gérard
5209547a89
Merge pull request #2514 from bagerard/enum_field_update
improvement to recent updates
2021-05-05 09:21:45 +02:00
Bastien Gerard
ecb5dda281 improvement to recent updates 2021-05-04 20:43:33 +02:00
Bastien Gérard
34245fe258
Merge pull request #2509 from jaesivsm/fix/EnumField-choices
Fixing EnumField choices validation
2021-05-04 20:33:25 +02:00
Bastien Gérard
04c26acdd6
Merge pull request #2513 from bagerard/prepare_release
Prepare release
2021-05-04 12:15:34 +02:00
Bastien Gerard
d0a15a8924 prepare changelog 2021-05-04 10:50:56 +02:00
Bastien Gerard
2215e2746b run latest autoformatter 2021-05-04 10:50:56 +02:00
Bastien Gerard
0e6bcbc030 update precommit 2021-05-04 10:50:56 +02:00
François Schmidts
ecf84cbd5d Fixing EnumField choices validation 2021-04-27 16:32:55 +02:00
Bastien Gérard
232071f8f4
Merge pull request #2505 from deveil/patch-1
Typo correction
2021-04-23 20:45:33 +02:00
Bastien Gérard
fc0fb31d43
Merge pull request #2503 from bagerard/precommit_update
housekeeping - pre-commit autoupdate
2021-04-23 20:44:34 +02:00
deveil
1bd8cd803e
Typo correction 2021-04-23 16:08:57 +02:00
Bastien Gerard
ef57a58155 housekeeping - pre-commit autoupdate 2021-04-22 10:18:15 +02:00
Bastien Gérard
9680259904
Merge pull request #2497 from bagerard/switch_to_isort
Switch to isort + add few pre-commit hooks
2021-03-31 14:11:23 +02:00
Bastien Gerard
49a4d23371 update hooks version 2021-03-29 22:04:07 +02:00
Bastien Gerard
b9d370c885 add standard hooks 2021-03-29 22:02:57 +02:00
Bastien Gerard
e5a2714baf update doc for isort 2021-03-29 21:55:34 +02:00
Bastien Gerard
ff596fcb7e replace flake8-import-order by isort 2021-03-29 21:50:22 +02:00
Bastien Gérard
f0fad6df19
Merge pull request #2485 from janste63/fix_2484
Test case and proposed solution for #2484
2021-03-29 20:52:53 +02:00
Jan Stein
da173cf0e2 Merge branch 'master' into fix_2484 2021-03-25 15:27:48 +01:00
Bastien Gérard
1669f0c5a4
Merge pull request #2489 from bagerard/fix_one_to_many_doc
Fix one-to-many example that is actually a many to many
2021-03-09 13:41:03 +01:00
Bastien Gérard
b045925efe
Merge pull request #2481 from bagerard/improve_connect_doc
improve connect() doc and put more emphasis on URI string
2021-03-09 13:40:45 +01:00
Bastien Gerard
b3ce65453a Fix one-to-many example that is actually a many to many 2021-03-08 22:21:48 +01:00
Bastien Gerard
50d891cb7b more improvement to connect doc 2021-03-08 21:52:14 +01:00
Bastien Gerard
e31f9150d2 improve connect() doc and put more emphasis on URI string 2021-03-08 21:51:43 +01:00
Bastien Gérard
74ceb9703b
Merge pull request #2488 from bagerard/add_lazy_loading_method_ease_profiling
Add _lazy_load_ref methods to make profiling easier
2021-03-08 19:18:54 +01:00
Bastien Gerard
58a3c6de03 Add _lazy_load_ref methods so that the impact of lazy loading surfaces when profiling 2021-03-08 00:14:53 +01:00
Bastien Gérard
86ad8d119d
Merge pull request #2487 from bagerard/prepare_0_23_0
update changelog for 0.23.0 release
2021-03-04 23:03:39 +01:00
Bastien Gerard
34d273015c update changelog for 0.23.0 release 2021-03-04 22:46:29 +01:00
Bastien Gérard
7147043d63
Merge pull request #2483 from bagerard/mongodb44_ci
Add Mongodb 44 ci and fix incompat
2021-03-04 22:42:13 +01:00
Bastien Gerard
b9b536133d fix test incompat for 4.2 2021-03-04 00:10:08 +01:00
Jan Stein
8fd969aba9 Test case and proposed solution for #2484 2021-03-03 14:58:03 +01:00
Bastien Gerard
f244207168 fix mongo download link for 4.2 4.4 explicitly 2021-03-03 12:26:48 +01:00
Bastien Gerard
0620ac5641 Fix mongo download link as convention changed in official repo with > 4.0 2021-03-03 11:33:36 +01:00
Bastien Gerard
3b9a167022 Add Mongo 4.4 to ci 2021-03-03 10:57:18 +01:00
Bastien Gerard
b479bb7c6b Fix tests for supporting Mongo4.4 for some reason results comes sorted differently in map reduce when no sort is specified 2021-03-03 10:52:46 +01:00
Bastien Gerard
8ef771912d fixing incompatibility with mongoengine aggregation to support mongo 4.4 2021-03-03 10:51:03 +01:00
Bastien Gérard
2d1c9afbb7
Merge pull request #2477 from lukasloetkolben/feature_allow_disk_use
Feature: "allow_disk_use"
2021-02-27 23:05:50 +01:00
Bastien Gerard
9ff5d8426c restrict test on allowDiskUse to mongoDB >= 4.4 in CI + ran black 2021-02-27 21:30:07 +01:00
lukasloetkolben
467e9c3ddf typo wether --> whether 2021-02-26 18:12:58 +01:00
lukasloetkolben
0d5e028c55 - tests for feature allow_disk_use 2021-02-26 18:11:32 +01:00
Bastien Gérard
5858ea1bf0
Merge pull request #2478 from bagerard/add_geonear_aggregation_test
Add $geonear aggregation test
2021-02-24 11:55:46 +01:00
Bastien Gerard
1f220b4eaf update changelog and fix minor typo in docstring 2021-02-23 21:51:41 +01:00
Bastien Gerard
97c99ca40d Add test case for #2473 2021-02-23 21:49:45 +01:00
lukasloetkolben
80a3b1c88c - feature: allow_disk_use 2021-02-22 22:06:10 +01:00
lukasloetkolben
68447af127 - feature: allow_disk_use 2021-02-22 21:42:02 +01:00
Bastien Gérard
d033e3b133
Merge pull request #2475 from bagerard/fix_imprecise_doc_collection_name
Fix for imprecise doc on collection name (CamelCase -> snake_case)
2021-02-22 19:52:28 +01:00
Bastien Gérard
4428842e77
Merge pull request #2476 from bagerard/fix_typo
fix_minor_typo
2021-02-22 19:52:08 +01:00
Bastien Gérard
f38cc6edd3
Merge pull request #2472 from vainu-jussi/master
Check that manually setting Sequence Field in DynamicDocument doesn't increment the counter
2021-02-21 22:39:56 +01:00
Bastien Gerard
aeb4f8f4da fix_minor_typo 2021-02-21 22:04:33 +01:00
Bastien Gerard
1b7c2085c9 Fix for imprecise doc on collection name (CamelCase -> snake_case) 2021-02-21 21:17:49 +01:00
Jussi Kauppila
48b979599f Check that manually setting Sequence Field in DynamicDocument doesn't increment the counter
Fixes #2471
2021-02-16 12:23:29 +02:00
Bastien Gérard
af3d3b7ee6
Merge pull request #2463 from PeterNerlich/patch-2
Fix inconsistency with example
2021-01-12 23:10:53 +01:00
Bastien Gérard
56fe126f3a
Merge pull request #2464 from bagerard/fix_coveralls_3_0_0
Fix coveralls in CI
2021-01-12 23:10:17 +01:00
Bastien Gerard
04905d4b37 Add env var to please python-coveralls 3.0.0 2021-01-12 23:07:11 +01:00
Peter Nerlich
460df112f4
Fix inconsistency with example
connection example shows a password of 12345, but text spoke of qwerty
2021-01-10 23:43:46 +01:00
Bastien Gérard
772096ec55
Merge pull request #2454 from bagerard/improve_test_field_coverage
improvements of test coverage in fields
2020-12-24 00:22:39 +01:00
Bastien Gerard
98d64f41c6 improvements of test coverage in fields 2020-12-18 00:05:40 +01:00
Bastien Gérard
9a3bca8ab6
Merge pull request #2453 from bagerard/prepare_0_22_1
Update changelog for 0.22.1
2020-12-16 23:07:18 +01:00
Bastien Gerard
5781753cc8 Update changelog for 0.22.1 2020-12-16 23:03:39 +01:00
Bastien Gérard
fd3699a519
Merge pull request #2452 from bagerard/github_actions_improvements
improve github actions
2020-12-16 22:51:36 +01:00
Bastien Gerard
4f6a24411d remove IRC notif from CI as it doesn't seem to be still valuable these days 2020-12-16 22:47:43 +01:00
Bastien Gerard
de3888a48b improve github actions 2020-12-16 22:45:45 +01:00
Bastien Gérard
700fe80a00
Merge pull request #2421 from bagerard/test_github_actions2
Migrate to GitHub actions
2020-12-16 22:36:19 +01:00
Bastien Gerard
49e33b978d move condition of build-n-publish to job level + disable test-pypi
releasing as there is some limitation on the size
2020-12-16 22:22:03 +01:00
Bastien Gerard
81197d6061 comment out travis file, until next release is fully operational from github actions 2020-12-16 22:07:34 +01:00
Bastien Gerard
aa368be4d3 minor comment improvement 2020-12-16 21:54:55 +01:00
Bastien Gerard
0f1fce4a7b Add separate jobs for test-pypi and pypi in github actions 2020-12-16 21:52:49 +01:00
Bastien Gérard
cc591a634a add coveralls to actions 2020-12-16 21:36:43 +01:00
Bastien Gérard
6e332e782b minor improvement to github actions 2020-12-16 21:21:22 +01:00
Bastien Gérard
0e9920b190 Build sphinx doc as part of ci 2020-12-16 21:21:22 +01:00
Bastien Gérard
fd35df07c4 add regular pypi deploy to github actions 2020-12-16 21:21:22 +01:00
Bastien Gérard
8f3d21c312 improve tag format match 2020-12-16 21:21:22 +01:00
Bastien Gérard
7b772e3a4a publish to test-pypi 2020-12-16 21:21:22 +01:00
Bastien Gérard
59438a4768 add IRC notif to github actions 2020-12-16 21:21:22 +01:00
Bastien Gérard
fe9f7f1f80 test github actions 2020-12-16 21:21:21 +01:00
Bastien Gérard
6b5231265c
Merge pull request #2449 from Kyle-Verhoog/drop-35
Drop Python 3.5 support
2020-12-15 09:18:49 +01:00
kyle-verhoog
0014346de1
Drop Python 3.5 support 2020-12-15 02:31:23 -05:00
Bastien Gérard
bc0e04d833
Merge pull request #2447 from bagerard/_changelog_0_22_0
prepare 0.22.0
2020-12-14 22:49:09 +01:00
Bastien Gerard
dd11911ed7 update changelog and bump version to 0.22.0 2020-12-14 22:39:48 +01:00
Bastien Gérard
0b3feedf94
Merge pull request #2446 from bagerard/improve_mongomock_tests
Improve mongomock tests to avoid regression
2020-12-14 21:24:33 +01:00
Bastien Gerard
65b8cfc96a Improve mongomock tests to avoid regression 2020-12-14 00:03:39 +01:00
Bastien Gérard
2e284b93b8
Merge pull request #2445 from bagerard/fix_count_mongomock
Use Queryset._query instead of Cursor.__spec for count()
2020-12-13 23:26:39 +01:00
Bastien Gerard
c28bb517cb Use Queryset._query instead of Cursor.__spec for count() 2020-12-13 10:43:26 +01:00
Bastien Gérard
aa3ff39ef8
Merge pull request #2444 from bagerard/improve_code_health
Improve code health
2020-12-13 10:27:37 +01:00
Bastien Gerard
149fb953e7 fix recent update not aligned with latest black 2020-12-13 00:16:10 +01:00
Bastien Gerard
bf12621ce9 update flake8 version in pre-commit 2020-12-13 00:14:53 +01:00
Bastien Gerard
8a1a68ea7c fix flake8 warnings 2020-12-13 00:14:53 +01:00
Bastien Gerard
eabb8f60f5 autoformat with pyupgrade 2020-12-13 00:14:53 +01:00
Bastien Gerard
c00a378776 autoformat with updated black 2020-12-13 00:14:52 +01:00
Bastien Gerard
ee6ef1ff4b autoformat w prettier 2020-12-13 00:14:52 +01:00
Bastien Gérard
fb8f02d0c0
Merge pull request #2441 from bagerard/remove_useless_cls_var
minor improvement in code
2020-12-08 23:22:57 +01:00
Bastien Gerard
a025199294 Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_useless_cls_var 2020-12-08 23:02:17 +01:00
Bastien Gérard
87babaaa30
Merge pull request #2439 from bagerard/improve_doc
Improve Fields documentation
2020-12-08 22:58:00 +01:00
Bastien Gerard
a4fff15491 minor improvement in code 2020-12-08 22:41:27 +01:00
Bastien Gerard
a190dfe2c4 additional improvements to fields constructor 2020-12-08 21:48:54 +01:00
Bastien Gerard
3926473917 Improve Fields documentation + remove versionadded/changed as it's not maintained 2020-12-06 22:25:12 +01:00
Bastien Gérard
9ffe0bcdee
Merge pull request #2432 from bagerard/remove_deprecated_landscape
remove landscape integration
2020-12-01 21:40:56 +01:00
Bastien Gérard
4fa3134294 remove landscape integration as it is dead 2020-11-29 21:15:02 +01:00
Bastien Gérard
92f6fce77d
Merge pull request #2431 from bagerard/remove_pillow_test_restriction
Remove restriction on Pillow version in tests
2020-11-29 20:48:13 +01:00
Bastien Gérard
b1a2cf061d update test requirement for pillow > 7.0 2020-11-29 10:22:59 +01:00
Bastien Gérard
0a05c1f590 Fix image size that needs to be forced in test 2020-11-29 10:08:58 +01:00
Bastien Gérard
7dbc217768 Remove restriction on Pillow version as it was there due to Py2 support 2020-11-28 22:27:24 +01:00
Bastien Gérard
bf411ab2ca Update changelog with recent changes that were merged 2020-11-28 22:21:51 +01:00
Bastien Gérard
277b827d4d
Merge pull request #2426 from volfpeter/master
Fix LazyReferenceField dereferencing in embedded documents
2020-11-26 21:45:12 +01:00
Peter Volf
e0bec881bc removed unused variable to fix a warning 2020-11-25 15:17:52 +01:00
Peter Volf
cc5e2ba054 fix LazyReferenceField dereferencing bug in embedded documents, refs #2375 2020-11-25 11:05:37 +01:00
Bastien Gérard
904fcd1a0a
Merge pull request #2424 from bagerard/bump_0_21_0
Prepare 0.21.0 release
2020-11-19 09:02:22 +01:00
Bastien Gérard
2ec454447f Bump version to 0.21.0 and update changelog 2020-11-18 22:28:06 +01:00
Bastien Gérard
ecd297e227
Merge pull request #2414 from bagerard/fix_db_fields_inconsistencies_in_constructor
Fix some issues related with db_field in constructor
2020-11-18 22:25:25 +01:00
Bastien Gérard
079ee3c191
Merge pull request #2417 from bagerard/add_migration_documentation
Add migration documentation
2020-11-18 22:19:19 +01:00
Bastien Gérard
f2638ecd02 update changelog 2020-11-14 15:31:39 +01:00
Bastien Gérard
ad6ff819fe Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_db_fields_inconsistencies_in_constructor 2020-11-14 14:44:08 +01:00
Bastien Gérard
48357640c6 improve deprecated pymongo call 2020-11-14 14:41:59 +01:00
Bastien Gérard
e6c2169f76
Merge pull request #2418 from bagerard/add_black_formatting_badge
Add black badge to readme
2020-11-14 13:42:57 +01:00
Bastien Gérard
1d17dc4663 Add black badge to readme to emphasize that repo is using autoformatter black as it is often forgotten in PR and makes CI failing 2020-11-12 22:13:53 +01:00
Bastien Gérard
eeac3bd2e6
Merge pull request #2416 from bagerard/remove_python_35
Remove Py3.5 as it is EOL and added 3.9 to CI
2020-11-12 09:47:44 +01:00
Bastien Gérard
3f5a15d236 improve changelog 2020-11-12 00:43:22 +01:00
Bastien Gérard
91493a1e79 improve migration doc 2020-11-12 00:40:52 +01:00
Bastien Gérard
0c274908ec Merge branch 'master' of github.com:MongoEngine/mongoengine into add_migration_documentation 2020-11-11 21:18:52 +01:00
Bastien Gérard
338c40b5d5 Remove Py3.5 as it is EOL and added 3.9 to CI 2020-11-11 21:14:54 +01:00
Bastien Gérard
fc3ccf9606
Merge pull request #2415 from bagerard/add_srv_uri_connect_doc
Document fact that srv URI can be used with host #1956
2020-11-11 21:05:52 +01:00
Bastien Gérard
746faceb5c Document fact that srv URI can be used with host #1956 2020-11-08 22:55:24 +01:00
Bastien Gérard
8c3058d99b Fix some issues related with db_field in constructor by removing field/db_field translation that shouldn't occur in constructor 2020-11-08 22:36:58 +01:00
Bastien Gérard
eb56fb9bda
Merge pull request #2413 from bagerard/dynamic_document_parsing_known_fields
Bug fix in DynamicDocument which is not parsing known fields
2020-11-08 13:17:07 +01:00
Bastien Gérard
161493c0d2
Merge pull request #2408 from bagerard/refactoring_remove_useless_code_only_fields
Removed code related to Document.__only_fields
2020-11-08 13:16:03 +01:00
Bastien Gérard
cb9f329d11
Merge pull request #2401 from SMASHDOCs/bugfix-save-sharding
Bugfix #2154
2020-11-07 21:43:49 +01:00
Bastien Gérard
03af784ebe Bug fix in DynamicDocument which isn not parsing known fields in constructor like Document do #2412 2020-11-07 21:30:23 +01:00
Felix Schultheiß
e5f6e4584a Merge commit master into bugfix-save-sharding 2020-11-03 10:05:31 +01:00
Felix Schultheiß
79f9f223d0 added to authors 2020-11-03 10:00:15 +01:00
Felix Schultheiß
0bc18cd6e1 fixed shard test case for old mongodb version 2020-11-03 10:00:02 +01:00
Felix Schultheiß
30a3c6a5b7 added testcase for save create with shard key 2020-11-02 17:30:24 +01:00
Bastien Gérard
90c5d83f84 remove deprecated comment 2020-11-02 15:02:11 +01:00
Bastien Gérard
d8b8ff6851 Removed code related to Document.__only_fields and Queryset.only_fields which appear to have no effect 2020-11-02 14:52:02 +01:00
Bastien Gérard
ee664f0c90
Merge pull request #2406 from bagerard/improve_enumfield_doc
improve EnumField Doc and add quick test
2020-11-01 23:08:30 +01:00
Bastien Gérard
f8d371229e tmp work on migration doc 2020-11-01 20:25:35 +01:00
Bastien Gérard
94a7e813b1 fix difference in test for certain version of pymongo 2020-11-01 19:37:13 +01:00
Bastien Gérard
8ef7213426 improve EnumField Doc and add quick test 2020-11-01 14:05:58 +01:00
Bastien Gérard
2f4464ead5
Merge pull request #2404 from mas15/add-enum-field
Add EnumField
2020-11-01 13:41:20 +01:00
Bastien Gérard
89b93461ac
Merge pull request #2405 from bagerard/remove_encoding_declarations
remove utf8 encoding declaration in test files
2020-11-01 13:16:57 +01:00
Mateusz Stankiewicz
9e40f3ae83 PR ammends 2020-10-31 10:47:20 +01:00
Bastien Gérard
f4962fbc40 remove utf8 encoding declaration in test files as it's not needed/recommended 2020-10-30 21:10:21 +01:00
Mateusz Stankiewicz
c9d53ca5d5 Add EnumField 2020-10-30 13:06:37 +01:00
Bastien Gérard
65f50fd713
Merge pull request #2387 from bagerard/fix_change_fields_inconsistencies
fix inconsistencies in ._changed_fields computation
2020-10-29 21:15:31 +01:00
Felix Schultheiß
bf1d04e399 black reformatting 2020-10-29 14:56:08 +01:00
Felix Schultheiß
5a8e5e5a40 updated docstring 2020-10-27 16:34:57 +01:00
Felix Schultheiß
f3919dd839 stripped out integrating shard key from _save_update, use it also in _save_create 2020-10-27 12:55:35 +01:00
Bastien Gérard
9f82a02ddf
Merge pull request #2106 from bagerard/add_validation_to_doc
Add a documentation page for validation
2020-10-20 00:27:05 +02:00
Bastien Gérard
015a36c85f minor styling fix in .rst 2020-10-19 23:59:12 +02:00
Bastien Gérard
fbd3388a59 Merge branch 'master' of github.com:MongoEngine/mongoengine into add_validation_to_doc 2020-10-19 23:36:12 +02:00
Bastien Gérard
d8a52d68c5 improve doc in .readthedocs.yml 2020-10-19 23:34:24 +02:00
Bastien Gérard
4286708e2e fix mongoengine setup.py path in .readthedocs.yml 2020-10-18 22:47:00 +02:00
Bastien Gérard
e362d089e1 install mongoengine for readthedocs build to work 2020-10-18 22:44:06 +02:00
Bastien Gérard
6b657886a5 remove explicit install from .readthedocs.yml to rely on default instead 2020-10-18 22:21:45 +02:00
Bastien Gérard
eb16945147 fix requirements.txt location for readthedocs 2020-10-18 22:06:15 +02:00
Bastien Gérard
38047ca992
Merge pull request #2396 from bagerard/fix_readthedocs_failed_build
Fix readthedocs build that failed
2020-10-18 22:04:01 +02:00
Bastien Gérard
c801e79d4b Fix readthedocs build that failed by making it use python3 instead of default python2.7 2020-10-18 21:33:30 +02:00
Bastien Gérard
3fca3739de rework validation documentation based on review 2020-10-18 21:11:16 +02:00
Bastien Gérard
c218c8bb6c Merge branch 'master' of github.com:MongoEngine/mongoengine into add_validation_to_doc 2020-10-17 15:05:27 +02:00
Bastien Gérard
0bbc05995a
Merge pull request #2393 from bagerard/fix_listfield_change_0
Fix listfield change detection of index 0
2020-10-11 10:15:18 +02:00
Bastien Gérard
3adb67901b update changelog for #2392 2020-10-11 00:53:46 +02:00
Bastien Gérard
d4350e7da4 Fix for ListField that isnt detecting properly that item 0 is changed 2020-10-10 23:32:22 +02:00
Bastien Gérard
4665658145
Merge pull request #2390 from bagerard/bump_latest_lib_ci
Upgrade pymongo and mongodb versions used in CI
2020-10-07 21:41:12 +02:00
Bastien Gérard
0d289fd5a1 upgrade pymongo and mongodb versions used in CI 2020-10-07 21:30:43 +02:00
Bastien Gérard
aabc18755c fix inconsistencies in ._changed_fields computation 2020-10-07 00:01:09 +02:00
Bastien Gérard
1f2a5db016 fix deprecated use of .update in test suite 2020-08-12 22:30:52 +02:00
Bastien Gérard
ff40f66291
Merge pull request #2243 from bagerard/fix_count_documents_deprecation
Fix count documents deprecation
2020-08-12 22:18:02 +02:00
Bastien Gérard
7f77084e0e minor fixes in doc links 2020-08-12 21:56:38 +02:00
Bastien Gérard
aca4de728e Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_count_documents_deprecation 2020-08-11 23:01:33 +02:00
Bastien Gérard
9e7ca43cad
Merge pull request #2365 from hiimdoublej/fix/queryTransform
Fix query transformation regarding special operators
2020-08-11 22:18:33 +02:00
Bastien Gérard
7116dec74a run black to please ci 2020-08-11 21:55:22 +02:00
Bastien Gérard
a5302b870b Merge branch 'fix/queryTransform' of git://github.com/hiimdoublej/mongoengine into hiimdoublej-fix/queryTransform 2020-08-11 21:48:00 +02:00
Bastien Gérard
604e9974b6
Merge pull request #2363 from bagerard/AttributeError_message_attr
fix py3 incompatible code
2020-08-03 21:37:36 +02:00
Johnny Chang
3e1c83f8fa Fix query transformation regarding special operators 2020-08-04 00:30:15 +08:00
Bastien Gérard
e431e27cb2 #2360 fix py3 incompatible code 2020-08-01 15:09:10 +02:00
Bastien Gérard
4f188655d0 Merge pull request #2335 from bagerard/fix_limit0_bug
Fix bug with Doc.objects.limit(0) which should return all docs
2020-05-27 09:43:35 +02:00
Bastien Gérard
194b0cac88 improve doc + changelog 2020-05-26 23:45:35 +02:00
Bastien Gérard
7b4175fc5c Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_limit0_bug 2020-05-26 23:44:05 +02:00
Bastien Gérard
adb5f74ddb Fix a bug in limit0 #2311 2020-05-26 23:37:55 +02:00
Bastien Gérard
107a1c34c8
Merge pull request #2331 from abarto/fix/clone-retain-read-preference-read-concern
Add read_concern to cloned properties. Add read_concern to aggregate().
2020-05-23 23:22:56 +02:00
Bastien Gérard
dc7da5204f Merge branch 'terencehonles-patch-1' 2020-05-23 23:12:33 +02:00
Bastien Gérard
0301bca176 Merge branch 'patch-1' of https://github.com/terencehonles/mongoengine into terencehonles-patch-1 2020-05-23 23:12:01 +02:00
Bastien Gérard
49f9bca23b fix black formatting 2020-05-23 23:08:56 +02:00
Agustin Barto
31498bd7dd Update changelog 2020-05-20 18:58:18 -03:00
Agustin Barto
1698f398eb Add _read_concern to copied properties. Add read_concern to aggregate. Add test to check the read_concern and read_preference values are kept after cloning. 2020-05-20 18:56:13 -03:00
Bastien Gérard
4275c2d7b7
Merge pull request #2330 from terencehonles/fix-empty-deprecation-warning-in-q-node
fix self inflicted deprecation warnings in QNode
2020-05-19 22:02:12 +02:00
Terence D. Honles
22bff8566d fix self inflicted deprecation warnings in QNode 2020-05-19 11:00:30 -07:00
Terence Honles
d8657be320
Fix requirement Pillow < 7 to mention it is for tests only 2020-05-19 10:23:07 -07:00
Bastien Gérard
3db9d58dac
Merge pull request #2319 from bagerard/prepare_0_20_0
Bump version to 0.20.0 and update CHANGELOG accordingly
2020-05-01 13:24:01 +02:00
Bastien Gérard
3fbe9c3cdd Bump version to 0.20.0 and update CHANGELOG accordingly 2020-05-01 13:13:30 +02:00
Bastien Gérard
130e9c519c
Merge pull request #2314 from bagerard/abarto-feature/allow-setting-read-concern-queryset
Abarto feature/allow setting read concern queryset
2020-04-26 22:57:57 +02:00
Bastien Gérard
78c9e9745d fix linting + update changelog & contributors 2020-04-26 22:51:56 +02:00
Bastien Gérard
38ebb5abf4
Merge branch 'master' into feature/allow-setting-read-concern-queryset 2020-04-26 22:36:47 +02:00
Bastien Gérard
9b73be26ab
Merge pull request #2313 from bagerard/DonQueso89-master
Improve docstring EmbeddedDocumentList (clone)
2020-04-26 22:33:39 +02:00
Bastien Gérard
fd0095b73f improve recent docstring of EmbeddedDocumentList.create 2020-04-26 22:28:14 +02:00
Bastien Gérard
226049f66a Merge branch 'master' of github.com:MongoEngine/mongoengine into DonQueso89-master 2020-04-26 22:27:11 +02:00
Bastien Gérard
dc1cf88ca6 Merge branch 'master' of git://github.com/DonQueso89/mongoengine into DonQueso89-master 2020-04-26 22:27:02 +02:00
Bastien Gérard
f5f8b730b5
Merge pull request #2254 from bagerard/fix_complex_datetime_field_invalid_string_set
fix complex datetime field invalid string set
2020-04-25 22:17:20 +02:00
Bastien Gérard
e8f6b42316 Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_complex_datetime_field_invalid_string_set 2020-04-25 22:12:35 +02:00
Bastien Gérard
49b0d73654
Merge pull request #2310 from bagerard/Pacu2-add-dev-tools
Pacu2 add dev tools
2020-04-25 21:53:50 +02:00
Bastien Gérard
394da67cf1 fix travis env var 2020-04-25 21:47:37 +02:00
Bastien Gérard
ef7da36ac6 Update pre-commit and fix existing flake8 warnings once for all 2020-04-25 21:36:07 +02:00
Bastien Gérard
1312100bc7 Merge branch 'master' of github.com:MongoEngine/mongoengine into Pacu2-fix/add-dev-tools 2020-04-25 20:53:35 +02:00
Bastien Gérard
4085bc2152 Merge branch 'fix/add-dev-tools' of git://github.com/Pacu2/mongoengine into Pacu2-fix/add-dev-tools 2020-04-25 20:46:44 +02:00
Bastien Gérard
f4d7e72426 improve .gitignore 2020-04-25 20:43:55 +02:00
Bastien Gérard
ece63ad071
Merge pull request #2279 from bagerard/drop_py2_support
Drop python 2 support
2020-04-25 14:22:23 +02:00
Bastien Gérard
a9550b8243
Merge pull request #2232 from bagerard/bump_development_status_classifier
Bump development status classifier
2020-04-25 14:17:46 +02:00
Bastien Gérard
43724e40b2 improve doc related to dropping Py2 support 2020-04-25 14:16:56 +02:00
Bastien Gérard
1bfa40e926 Merge branch 'master' of github.com:MongoEngine/mongoengine into bump_development_status_classifier 2020-04-25 14:07:43 +02:00
Bastien Gérard
d493f71c4e Merge branch 'drop_py2_support' of github.com:bagerard/mongoengine into drop_py2_support 2020-04-25 14:05:30 +02:00
Bastien Gérard
87f4d1a323 Merge branch 'master' of github.com:MongoEngine/mongoengine into drop_py2_support 2020-04-25 13:51:21 +02:00
Bastien Gérard
0a0e6114f5
Merge branch 'master' into drop_py2_support 2020-04-19 23:13:36 +02:00
Bastien Gérard
41d36fa3bf
Merge pull request #2304 from bagerard/fix_py2_configparser
fix subdependencies that dropped Py2 support
2020-04-09 09:20:39 +02:00
Bastien Gérard
707923e3f5 fix subdependencies that dropped Py2 support 2020-04-08 21:54:26 +02:00
Bastien Gérard
d9b9581df2 Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_complex_datetime_field_invalid_string_set 2020-04-08 21:33:58 +02:00
Bastien Gérard
463e7c66af
Merge pull request #2287 from bagerard/fix_queryset_get_error_message_multiple_match
Improve MultipleObjectsReturned message from queryset.get
2020-04-08 21:28:56 +02:00
Filip Kucharczyk
2be28a22a7
Merge branch 'master' into fix/add-dev-tools 2020-03-27 15:15:14 +01:00
Kes
d73f0bb1af
fix typo 2020-03-26 17:25:25 +01:00
Kes
ce74978b1e
Correct and improve docstring on EmbeddedDocumentList.create() 2020-03-26 17:23:16 +01:00
Bastien Gérard
2b0157aecd Improve Queryset.get to avoid confusing message in case multiple match are found 2020-03-22 14:08:29 +01:00
Bastien Gérard
f49baf5d90
Merge pull request #2285 from bagerard/remove_drop_dups
Remove drop_dups that was deprecated with Mongo3
2020-03-18 21:02:03 +01:00
Agustin Barto
7cc964c7d8 Add missing import 2020-03-17 21:58:36 -03:00
Agustin Barto
bc77322c2f Update unit tests 2020-03-17 21:49:17 -03:00
Agustin Barto
8913a74a86 Allow setting the read concern to None 2020-03-17 21:31:05 -03:00
Agustin Barto
af35b25d15 Refactor read_preference to accept a dictionary instead of a ReadPreference instance. 2020-03-17 21:28:08 -03:00
Bastien Gérard
476b07af6e reformat changelog 2020-03-17 23:59:54 +01:00
Bastien Gérard
e2b9a02531 Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_drop_dups 2020-03-17 23:58:44 +01:00
Bastien Gérard
6cc6229066
Merge pull request #2245 from bagerard/remove_old_deprecated_method
[>= 0.19.0] Remove old deprecated method
2020-03-17 23:51:42 +01:00
Agustin Barto
4c62a060f0 Add tests for set_write_conern and set_read_write_concern 2020-03-17 19:37:21 -03:00
Agustin Barto
3d80637fa4 Refactor set_read_write_concern so read_conern is consistent with the write_concerns argument. 2020-03-17 19:08:43 -03:00
Bastien Gérard
68be9fe979
Merge pull request #2284 from bagerard/remove_field_name_attribute_deprecated
Remove name param in Field constructor (replaced by db_field)
2020-03-17 21:46:38 +01:00
Bastien Gérard
547cd4a3ae Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_old_deprecated_method 2020-03-17 21:39:36 +01:00
Bastien Gérard
ee2d50b2d1 remove drop_dups index option, deprecated with MongoDB3 2020-03-17 21:38:50 +01:00
Bastien Gérard
15c3ddece8 Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_field_name_attribute_deprecated 2020-03-17 21:29:47 +01:00
Bastien Gérard
beaa9744b7
Merge pull request #2283 from bagerard/remove_slave_okay_deprecated
remove qs.slave_okay() that is deprecated since pymongo3
2020-03-17 21:27:50 +01:00
Bastien Gérard
8eb51790b5 Remove Field(name='...') which was deprecated when db_field was introduced a while ago 2020-03-17 21:26:41 +01:00
Bastien Gérard
aadc6262ed remove qs.slave_okay() that is deprecated since pymongo3 2020-03-17 21:10:52 +01:00
Bastien Gérard
00ae6298d4
Merge pull request #2242 from bagerard/fix_dictfield_validation
Fix bug introduced in 0.19 related to DictField validation
2020-03-17 20:26:54 +01:00
Bastien Gérard
ad0669a326 update changelog 2020-03-17 10:15:47 +01:00
Bastien Gérard
85df76c623 Merge branch 'master' of github.com:MongoEngine/mongoengine into drop_py2_support 2020-03-17 09:24:32 +01:00
Bastien Gérard
87512246cb Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_dictfield_validation 2020-03-16 22:30:48 +01:00
Bastien Gérard
a3f9016ae9 reformat import for flake8 2020-03-15 22:27:19 +01:00
Bastien Gérard
4e58e9f8d1 Merge branch 'master' of github.com:MongoEngine/mongoengine into feature/allow-setting-read-concern-queryset 2020-03-15 22:25:59 +01:00
Bastien Gérard
7c533394fd
Merge pull request #2267 from abarto/support-elemmatch-projection
Add support for the elemMatch projection operator
2020-03-15 22:25:08 +01:00
Bastien Gérard
333e014f13 Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_complex_datetime_field_invalid_string_set 2020-03-15 22:20:46 +01:00
Bastien Gérard
c0c0efce18 improve docstring related to #2267 and document the change in the changelog 2020-03-15 22:14:26 +01:00
Bastien Gérard
beabaee345 Merge branch 'master' of github.com:MongoEngine/mongoengine into support-elemmatch-projection 2020-03-15 22:08:20 +01:00
Bastien Gérard
c937af3919
Merge pull request #2281 from bagerard/fix_py38_deprecation_warn
Fix invalid escape seq in codebase
2020-03-15 21:16:54 +01:00
Bastien Gérard
aa4a6ae023 Fix invalid escape seq in codebase 2020-03-15 21:02:44 +01:00
Bastien Gérard
b57946ec98 remove virtualenv installation in travis 2020-03-14 21:39:47 +01:00
Bastien Gérard
1e110a2c41 run pyupgrade --py3-plus 2020-03-11 23:34:43 +01:00
Bastien Gérard
b234aa48e4 run pyupgrade 2020-03-11 23:21:38 +01:00
Bastien Gérard
8086576677 get rid of six 2020-03-11 23:07:03 +01:00
Bastien Gérard
03e34299f0 clean code related to Py2 + six.text_type & six.string_types 2020-03-11 21:50:50 +01:00
Bastien Gérard
421e3f324f Drop python2 support 2020-03-09 00:18:52 +01:00
Bastien Gérard
a0b803959c Merge branch 'master' of github.com:MongoEngine/mongoengine into py2py3_improve_compat 2020-03-08 14:44:39 +01:00
Bastien Gérard
ff4d57032a reformat w black 2020-03-03 23:51:11 +01:00
Bastien Gérard
ba34589065 Merge branch 'master' of github.com:MongoEngine/mongoengine into abarto-feature/allow-setting-read-concern-queryset 2020-03-03 23:50:36 +01:00
Bastien Gérard
a4d11eef46
Merge pull request #2264 from Pacu2/2263/combining-raw-and-regular-queries
Fix for combining raw and regular filters
2020-03-03 23:48:31 +01:00
Filip Kucharczyk
fda2e2b47a Update changelog 2020-03-03 15:18:58 +01:00
Filip Kucharczyk
d287f480e5 Fix for combining raw and regular filters 2020-03-03 15:18:58 +01:00
Bastien Gérard
d85f0e6226 Merge branch 'feature/allow-setting-read-concern-queryset' of git://github.com/abarto/mongoengine into abarto-feature/allow-setting-read-concern-queryset 2020-03-02 23:04:42 +01:00
Bastien Gérard
cfb4943986 reformat with black 2020-03-02 22:49:21 +01:00
Bastien Gérard
b453a96211
Merge pull request #2272 from leodmgs/add-return-docstr-to-save-function
Add return info in the save function docstring
2020-03-02 21:27:39 +01:00
Leonardo Domingues
81f9b351b3 Add return info in the save function docstring 2020-02-21 19:14:34 -03:00
Agustin Barto
4bca3de42f Add support for the elemMatch projection operator. Add basic tests to the fields queryset method. 2020-02-14 16:43:07 -03:00
Bastien Gérard
235b1a3679
Merge pull request #2265 from bagerard/fix_ci_lib_drop_python2_support
fix indirect library version (zipp) that dropped python2 support recently
2020-02-08 13:45:13 +01:00
Bastien Gérard
450658d7ac fix indirect library version that dropped python2 support recently 2020-02-04 22:51:02 +01:00
Agustin
8e17e42e26 Allow setting read_concern 2020-01-24 13:11:07 -03:00
Bastien Gérard
2d6a4c4b90 update changelog 2020-01-23 23:36:03 +01:00
Bastien Gérard
38703acc29 fix complex datetime field invalid string set 2020-01-23 23:33:23 +01:00
Bastien Gérard
095217e797 remove methods that were derecated in 2013... 2020-01-13 23:53:24 +01:00
Bastien Gérard
86e965f854 remove very old deprecated method 2020-01-13 23:45:42 +01:00
Bastien Gérard
57db68dc04 update changelog 2020-01-13 23:34:43 +01:00
Bastien Gérard
72de6d67c7 Bump dev status classifier 2020-01-13 23:33:02 +01:00
Bastien Gérard
b2c3acd025
Merge pull request #2244 from bagerard/mongodb_40
Mongodb 40
2020-01-13 23:26:11 +01:00
Bastien Gérard
605de59bd0 improve travis + fix tox mg310 2020-01-12 21:37:32 +01:00
Bastien Gérard
e0565ddac5 update changelog 2020-01-12 21:31:28 +01:00
Bastien Gérard
18b68f1b80 update travis mongo 4.0 to latest 4.0.13 2020-01-12 21:29:18 +01:00
Bastien Gérard
ea88806630 Merge branch 'master' of github.com:MongoEngine/mongoengine into mongodb_40 2020-01-12 21:16:48 +01:00
Bastien Gérard
412bed0f6d fix bug in legacy .count due to with_limit_and_skip that was missing 2020-01-12 11:04:05 +01:00
Bastien Gérard
53cf26b9af Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_count_documents_deprecation 2020-01-12 10:07:36 +01:00
Bastien Gérard
d738462139 Fix bug introduced in -1.19 related to DictField validate failing without default connection 2020-01-11 23:17:05 +01:00
Bastien Gérard
2fa48cd9e5 fix for pymongo < 3.7 2020-01-07 22:24:55 +01:00
Bastien Gérard
e64a7a9448 reformat with latest black 2020-01-07 22:11:24 +01:00
Bastien Gérard
9490ad2bf7
Merge pull request #2238 from bagerard/test_latest_pymongo
include latest pymongo version in travis
2020-01-07 09:35:49 +01:00
Bastien Gérard
84f3dce492 fix flake8 findings 2020-01-05 22:50:19 +01:00
Bastien Gérard
60c42dddd5 finalize code related to count_documents migration 2020-01-05 22:29:13 +01:00
Bastien Gérard
f93f9406ee improve doc next to code 2020-01-05 21:08:20 +01:00
Bastien Gérard
705c55ce24 update tox file to account for mg310 2020-01-05 21:01:17 +01:00
Bastien Gérard
928770c43a switching to count_documents 2020-01-05 21:01:03 +01:00
Bastien Gérard
59fbd505a0 include latest pymongo version in travis 2020-01-05 20:20:13 +01:00
Bastien Gérard
1cc20c9770
Merge pull request #2237 from timgates42/bugfix/typo_thoroughly
Fix simple typo: thorougly -> thoroughly
2020-01-05 11:17:49 +01:00
Tim Gates
f8f267a880
Fix simple typo: thorougly -> thoroughly
Closes #2236
2020-01-05 20:48:16 +11:00
erdenezul
80ea1f6883
Merge pull request #2233 from bagerard/release_0_19_1
Bump version to 0.19.1 + force pillow to be < 7.0.0
2020-01-03 21:11:17 +01:00
Bastien Gérard
75ee282a3d black setup.py to please CI 2020-01-03 14:15:24 +01:00
Bastien Gérard
4edad4601c Bump version to 0.19.1 + force pillow to be < 7.0.0 2020-01-03 14:03:17 +01:00
Bastien Gérard
152b51fd33 improve gridfs example (properly opening file) 2019-12-29 14:36:50 +01:00
erdenezul
66a0fca4ad
Merge pull request #2229 from bagerard/improve_doc_gridfs
improve doc of GridFS
2019-12-27 16:46:38 +01:00
Bastien Gérard
e7c7a66cd1 improve doc of GridFS, emphasize that subsequent call to read() requires to rewind the file with seek(0) 2019-12-27 10:38:03 +01:00
Bastien Gérard
b3dbb87c3c improve doc of aggregate kwargs 2019-12-27 10:06:27 +01:00
erdenezul
3d45538998
Merge pull request #2227 from bagerard/test_py38
Test py38
2019-12-27 07:05:12 +01:00
erdenezul
8df9d3fef9
Merge pull request #2228 from bagerard/change_aggregate_interface
change & deprecate .aggregate interface to mimic pymongo's interface
2019-12-27 07:04:25 +01:00
Bastien Gérard
99e660c66d update changelog 2019-12-27 09:32:05 +01:00
Bastien Gérard
aa02f87b69 change & deprecate .aggregate api to mimic pymongo's interface + separate the aggregation tests from the large test_queryset.py file 2019-12-27 09:23:15 +01:00
Bastien Gérard
f0d1ee2cb4 update travis config + improve readmecode 2019-12-26 21:03:34 +01:00
Bastien Gérard
ca4967311d update python 3.8 config 2019-12-26 21:00:34 +01:00
Bastien Gérard
65eb6ab611 Merge branch 'master' of github.com:MongoEngine/mongoengine into test_py38 2019-12-26 20:58:31 +01:00
Bastien Gérard
1cb2f7814c Merge branch 'mcsimps2-allow-periods-in-dict' 2019-12-26 20:53:50 +01:00
Bastien Gérard
b5485b16e6 Merge branch 'allow-periods-in-dict' of https://github.com/mcsimps2/mongoengine into mcsimps2-allow-periods-in-dict 2019-12-26 20:47:46 +01:00
Bastien Gérard
62c8597a3b fix pypi deployment version match 2019-12-24 11:15:23 +01:00
Bastien Gérard
488604ff2e test python 3.8 2019-12-24 00:00:15 +01:00
erdenezul
bd88a17b8e
Merge pull request #2224 from bagerard/release_0_19_0
Release 0 19 0
2019-12-22 18:46:38 +01:00
Bastien Gérard
8e892dccfe document recent merged PR in changelog 2019-12-20 23:51:01 +01:00
Bastien Gérard
c22eb34017 Merge branch 'master' of github.com:MongoEngine/mongoengine into release_0_19_0 2019-12-20 23:49:41 +01:00
Bastien Gérard
dcf3edb03e
Merge pull request #2094 from divomen/negative_indexes_in_list
Supported updates of an array by negative index
2019-12-20 23:48:00 +01:00
Bastien Gérard
c85b59d3b5 Merge branch 'master' of github.com:MongoEngine/mongoengine into negative_indexes_in_list 2019-12-20 23:27:51 +01:00
Bastien Gérard
1170de1e8e added explicit doc for order_by #2117 2019-12-20 23:20:15 +01:00
Bastien Gérard
332bd767d4 minor fixes in tests 2019-12-20 23:20:15 +01:00
erdenezul
0053b30237
Merge pull request #2217 from bagerard/improve_inheritance_doc
provide additional details on how inheritance works in doc
2019-12-20 22:43:45 +01:00
Bastien Gérard
d44533d956 completed the changelog with missing details 2019-12-20 22:41:22 +01:00
Bastien Gérard
12d8bd5a22 bump version to 0.19.0 2019-12-20 22:11:43 +01:00
Bastien Gérard
ae326678ec updated changelog for upcoming 0.19.0 2019-12-20 22:09:04 +01:00
Bastien Gérard
8d31f165c0
Merge pull request #2182 from daewok/fix-embedded-document-equality-with-lazy-refs
Fix EmbeddedDocument equality with LazyReferenceFields
2019-12-16 23:57:50 +01:00
Eric Timmons
cfd4d6a161 Add breaking change to changelog for LazyReferenceField representation in _data 2019-12-15 20:46:48 -05:00
Eric Timmons
329f030a41 Always store a DBRef, Document, or EmbeddedDocument in LazyReferenceField._data
This is required to handle the case of equality tests on a LazyReferenceField
with dbref=True when comparing against a field instantiated with an ObjectId.
2019-12-15 20:46:48 -05:00
Eric Timmons
68dc2925fb Add LazyReferenceField with dbref=True to embedded_document equality test 2019-12-15 20:21:22 -05:00
Eric Timmons
0d4e61d489 Add daewok to AUTHORS per contributing guidelines 2019-12-15 20:21:18 -05:00
Eric Timmons
dc7b96a569 Make python value for LazyReferenceFields be a DBRef
Previously, when reading a LazyReferenceField from the DB, it was stored
internally in the parent document's _data field as an ObjectId. However, this
meant that equality tests using an enclosing EmbeddedDocument would not return
True when the EmbeddedDocument being compared to contained a DBRef or Document
in _data. Enclosing Documents were largely unaffected because they look at the
primary key for equality (which EmbeddedDocuments lack).

This makes the internal Python representation of a LazyReferenceField (before
the LazyReference itself has been constructed) a DBRef, using code identical to
ReferenceField.
2019-12-15 20:20:59 -05:00
Eric Timmons
50882e5bb0 Add failing test
Test that __eq__ for EmbeddedDocuments with LazyReferenceFields works as
expected.
2019-12-15 20:20:59 -05:00
Bastien Gérard
280a73af3b minor fix in doc of NULLIFY to improve #834 2019-12-14 21:44:59 +01:00
Bastien Gérard
d8c0631dab added entry in changelog for py2py3 compatibility 2019-12-14 21:23:28 +01:00
Bastien Gérard
9166ba91d7 fix minor styling issue 2019-12-14 16:11:59 +01:00
Bastien Gérard
6bc4e602bb additional fix 2019-12-14 00:05:48 +01:00
Bastien Gérard
45a7520fc3 make use past.builtins.long 2019-12-13 23:57:18 +01:00
Bastien Gérard
64c0cace85 fix from code review regarding py2py3 compat 2019-12-13 23:57:18 +01:00
Bastien Gérard
82af5e4a19 fix small finding from review on py2py3 compat 2019-12-13 23:54:42 +01:00
Bastien Gérard
7e0ba1b335 clean remaining references to 2to3 2019-12-13 23:52:59 +01:00
Bastien Gérard
44b7f792fe fix benchmarks prints 2019-12-13 23:52:59 +01:00
Bastien Gérard
a3e432eb68 remove references to '2to3' in doc, travis, etc 2019-12-13 23:46:13 +01:00
Bastien Gérard
009f9a2b14 set dist as xenial to avoid relying on flaky travis default dist 2019-12-13 23:43:04 +01:00
Bastien Gérard
2ca905b6e5 Finalize python2/3 codebase compatibility and get rid of 2to3 2019-12-13 23:43:04 +01:00
Bastien Gérard
3b099f936a provide additional details on how inheritance works in doc 2019-12-13 21:32:45 +01:00
Bastien Gérard
4d6ddb070e
Merge pull request #2210 from Pacu2/fix/implement-bool-on-q-and-qcombination
Add __bool__ to Q and QCombination
2019-12-13 20:53:33 +01:00
Matt Simpson
b205314424 Add ability for dict keys to have . or $ in MongoDB >= 3.6
Starting in MongoDB >= 3.6, it is valid for dictionary keys to have $ or . in them as long as they don't start with $.  Additional tests added.
2019-12-10 11:09:29 -05:00
Filip Kucharczyk
e83132f32c Note deprecation of 'empty' in changelog 2019-12-10 11:54:52 +01:00
Filip Kucharczyk
1b38309d70 Revert 'empty' usage to it's previous state 2019-12-06 10:14:22 +01:00
Filip Kucharczyk
6e8196d475 Python 2.x compatibility 2019-12-05 01:41:48 +01:00
Filip Kucharczyk
90fecc56dd Reformat with black 2019-12-05 01:33:35 +01:00
Filip Kucharczyk
d3d7f0e670 Changelog 2019-12-05 01:18:29 +01:00
Filip Kucharczyk
37ffeafeff Replace 'pip' with 'python -m pip install' in docs 2019-12-05 01:13:05 +01:00
Filip Kucharczyk
abc159b7b9 Update Contributing, changelog 2019-12-05 01:10:54 +01:00
Filip Kucharczyk
648b28876d Rename requirements-lint.txt to requirements-dev.txt 2019-12-05 00:57:17 +01:00
Filip Kucharczyk
5b9f2bac87 Add pre-commit 2019-12-05 00:54:20 +01:00
Filip Kucharczyk
17151f67c2 Reformat repo with pre-commit hooks 2019-12-05 00:53:39 +01:00
Filip Kucharczyk
5f14d958ac Sort imports 2019-12-05 00:46:57 +01:00
Filip Kucharczyk
bd6c52e025 Changelog 2019-12-05 00:30:03 +01:00
Filip Kucharczyk
cb77bb6b69 Implement __bool__ on Q and QCombination 2019-12-05 00:27:41 +01:00
Bastien Gérard
78b240b740 updated changelog + improved query_counter test 2019-12-04 21:51:07 +01:00
Bastien Gérard
7e30f00178
Merge pull request #2195 from EloiZalczer/master
Added alias parameter in query_counter
2019-12-04 21:50:41 +01:00
Bastien Gérard
35310dbc73
Merge pull request #2204 from Pacu2/2203/ability-to-compare-q-objects
Add __eq__ to Q and Q operations
2019-12-04 20:58:04 +01:00
Filip Kucharczyk
af82c07acc Reformat with black 2019-12-03 09:19:02 +01:00
Filip Kucharczyk
3f75f30f26 Run black 2019-12-03 09:03:49 +01:00
Filip Kucharczyk
f7f0e10d4d Update changelog 2019-12-03 00:54:53 +01:00
Filip Kucharczyk
091238a2cf Update Authors 2019-12-03 00:54:46 +01:00
Filip Kucharczyk
0458ef869e Add __eq__ to Q and Q operations 2019-12-03 00:44:13 +01:00
Eloi Zalczer
0bf08db7b9 Added test case for query_counter alias 2019-12-02 10:07:33 +01:00
Eloi Zalczer
d3420918cd Added alias parameter in query_counter 2019-11-18 17:16:06 +01:00
Bastien Gérard
138e759161
Merge pull request #2187 from bagerard/improve_error_message_InvalidDocumentError
Improve error message from InvalidDocumentError
2019-11-08 21:27:17 +01:00
Bastien Gérard
f1d6ce7d12
Merge pull request #2189 from jangop/patch-1
Specify version of requirement
2019-11-08 21:25:41 +01:00
Jan Philip Göpfert
ff749a7a0a
Specify version of requirement
In `README.rst`, a version of `six` of at least `1.10.0` is specified. This was missing from the requirements, potentially leading to broken installations.
2019-11-06 10:35:16 +01:00
Bastien Gérard
bff78ca8dd
Merge pull request #2188 from bagerard/convert_assertions
Convert assertions
2019-11-05 20:06:59 +01:00
Bastien Gérard
81647d67a0 fix recent tests update with unittest2pytest 2019-10-31 23:06:40 +01:00
Bastien Gérard
d8924ed892 remove inheritance from unittest.TestCase on basic test classes 2019-10-31 23:02:28 +01:00
Bastien Gérard
799cdafae6 remove references to nose 2019-10-31 23:02:28 +01:00
Bastien Gérard
bc0c55e49a improve tests health (flake8 warnings) 2019-10-31 23:01:27 +01:00
Bastien Gérard
c61c6a8525 fix == None assertions 2019-10-31 22:59:49 +01:00
Bastien Gérard
3e764d068c fix remaining assertRaises 2019-10-31 22:59:49 +01:00
Bastien Gérard
ac25f4b98b ran unittest2pytest 2019-10-31 22:59:49 +01:00
Bastien Gérard
aa6ff8c84a
Merge pull request #2157 from bagerard/switch_pytest
Switch to pytest as runner
2019-10-31 22:50:39 +01:00
Bastien Gérard
37ca79e9c5 fix black formatting 2019-10-31 22:39:53 +01:00
Bastien Gérard
6040b4b494 update changelog 2019-10-31 21:33:19 +01:00
Bastien Gérard
51ea3e3c6f fix for recent coverage/coveralls compatibility issue 2019-10-31 20:39:49 +01:00
Bastien Gérard
5a16dda50d fix coverage for pytest runner 2019-10-31 20:39:49 +01:00
Bastien Gérard
bbfa978861 switch test runner from nose to pytest 2019-10-31 20:39:49 +01:00
Bastien Gérard
54ca7bf09f fix associated test to avoid discrepencies btw py2 and py3 2019-10-28 23:33:37 +01:00
Bastien Gérard
8bf5370b6c Improve error message from InvalidDocumentError whenever an embedded document has a bad shape (e.g due to migration) 2019-10-28 22:05:13 +01:00
Bastien Gérard
ecefa05e03
Merge pull request #2174 from bagerard/upgrade_latest_pymongo_travis
add latest pymongo 3.9 as part of the CI
2019-10-28 21:22:00 +01:00
erdenezul
e013494fb2
Merge pull request #2177 from bagerard/fix_few_deprecation_warnings
fix 2 pymongo deprecation warnings
2019-10-22 15:56:39 +02:00
erdenezul
4853f74dbf
Merge pull request #2184 from bagerard/docs_logging
Documented how pymongo.monitoring can be used with MongoEngine
2019-10-21 20:07:42 +08:00
erdenezul
6f45ee6813
Merge pull request #2176 from bagerard/fix_docstring_pycharm
Fix docstring format to improve pycharm inspection
2019-10-21 20:06:52 +08:00
Bastien Gérard
c60ed32f3a Documented how pymongo.monitoring can be used with MongoEngine 2019-10-16 21:25:17 +02:00
Bastien Gérard
178851589d
Merge pull request #2175 from bagerard/document_no_async_driver_support
Document no async driver support
2019-10-16 20:34:26 +02:00
Bastien Gérard
5bcc679194 fix 2 pymongo deprecation warnings 2019-10-10 22:55:44 +02:00
Bastien Gérard
1e17b5ac66 Fix docstring format to improve pycharm inspection 2019-10-05 14:24:54 +02:00
Bastien Gérard
19f12f3f2f document pymongo in RTD and make it point to github 2019-10-04 21:51:12 +02:00
Bastien Gérard
71e8d9a490 Added a FAQ to doc and Document the fact that we dont support motor 2019-10-04 21:51:12 +02:00
Bastien Gérard
e3cd553f82 add latest pymongo 3.9 as part of the CI 2019-10-04 21:46:47 +02:00
Bastien Gérard
b61c8cd104 fix tox envs 2019-10-01 22:17:19 +02:00
Bastien Gérard
8f288fe458 add mongodb 4.0 to travis and docs 2019-09-29 22:50:14 +02:00
erdenezul
02a920feea
Merge pull request #2166 from bagerard/add_aggregate_tutorial
Document Model.objects.aggregate entrypoint with an example
2019-09-17 21:07:42 +02:00
Bastien Gérard
be2c4f2b3c fix formatting and improve doc based on review 2019-09-16 21:15:35 +02:00
Bastien Gérard
7ac74b1c1f Document Model.objects.aggregate entrypoint with an example 2019-09-15 23:27:34 +02:00
Bastien Gérard
933cb1d5c7
Merge pull request #2144 from 0a69911a/master
Implement collation for queryset
2019-09-11 23:27:25 +02:00
Bastien Gérard
6203e30152
Merge pull request #2160 from vainu-arto/fix-trust-default-cursor-timeout
Only set no_cursor_timeout when requested
2019-09-11 22:09:56 +02:00
Bastien Gérard
7d94af0e31 add test coverage for no_cursor_timeout to support recent fix 2019-09-11 21:53:30 +02:00
erdenezul
564a2b5f1e
Merge pull request #2153 from erdenezul/misleading_bulkwrite_error
Change misleading error message
2019-09-04 09:42:10 +02:00
Erdenezul Batmunkh
1dbe7a3163 Add log in changelog 2019-09-03 16:17:09 +02:00
Arto Jantunen
47f8a126ca Only set no_cursor_timeout when requested
Previously this was always set for all requests. The parameter is only
documented as supported for certain queries, so this was probably wrong.

Mongo version 4.2 fails update queries that have this parameter set making
mongoengine unusable there. Fixes #2148.
2019-09-03 14:36:06 +03:00
Bastien Gérard
693195f70b fix test_pickable that was brought back to life recently 2019-08-31 22:28:31 +03:00
Bastien Gérard
2267b7e7d7 rename remaining files for pytest migration 2019-08-30 16:27:56 +03:00
Erdenezul Batmunkh
a06e605e67 Add BulkWriteError exception 2019-08-29 11:11:27 +02:00
otrofimov
47c67ecc99 Merge branch 'master' of https://github.com/MongoEngine/mongoengine 2019-08-29 11:48:07 +03:00
Bastien Gérard
4c4b7cbeae
Merge pull request #2156 from bagerard/rename_test_files_for_pytest_migration
rename all test files so that they are prefixed by test
2019-08-28 16:17:38 +03:00
Bastien Gérard
ddececbfea rename all test files so that they are prefixed by test_{orginal_filename}.py 2019-08-28 16:01:44 +03:00
otrofimov
71a6f3d1a4 test_collation:
Added test with `pymongo.collation.Collation` object
Readable list of BlogPost names for test
2019-08-21 18:26:10 +03:00
Erdenezul Batmunkh
e86cf962e9 Change misleading error message 2019-08-21 13:08:30 +02:00
erdenezul
99a58d5c91
Merge pull request #2149 from AliMirlou/patch-1
Fix small typo
2019-08-21 11:04:20 +02:00
Ali Mirlou
eecbb5ca90
Fix small typo 2019-08-20 19:53:49 +04:30
otrofimov
fbb3bf869c compatibility with black 2019-08-08 15:56:20 +03:00
otrofimov
b887ea9623 Implement collation for queryset 2019-08-08 11:55:45 +03:00
Bastien Gérard
c68e3e1238 Add test case for list update by negative index 2019-07-24 21:37:16 +02:00
Bastien Gérard
c5080e4030 Merge branch 'master' of github.com:MongoEngine/mongoengine into negative_indexes_in_list 2019-07-24 21:14:55 +02:00
Bastien Gérard
0d01365751
Merge pull request #2131 from NMFR/master
Set default database when using mongomock.
2019-07-22 11:56:56 +02:00
Nuno Rodrigues
f4a06ad65d Optimize database name getter calls 2019-07-22 08:45:35 +00:00
Nuno Rodrigues
05a22d5a54 Extract test to its own method 2019-07-22 08:42:17 +00:00
Nuno Rodrigues
2424ece0c5 Fix a linting error 2019-07-16 13:40:46 +00:00
Nuno Rodrigues
2d02551d0a Add test 2019-07-15 17:54:46 +01:00
Stefan Wójcik
ac416aeeb3
Improve BaseDocument.from_json tests and documentation (#2127) 2019-07-15 12:15:55 +02:00
Stefan Wojcik
d09af430e8 Fix formatting of the changelog – part 2
RTD didn't render it properly previously.

Refs #2105
2019-07-15 09:57:48 +02:00
Stefan Wojcik
79454b5eed Fix formatting of the changelog
RTD didn't render it properly previously.

Refs #2105
2019-07-15 09:54:18 +02:00
Stefan Wojcik
921c1fa412 Dummy commit to confirm that the new RTD webhook works 2019-07-15 09:50:29 +02:00
Stefan Wojcik
1aba145bc6 Split requirements into requirements.txt and requirements-lint.txt
I'm doing this because it makes sense to separate which requirements are
needed to develop the package and which are purely needed for static code
analysis. That said, the trigger for this commit was that ReadTheDocs
automatically tries to install everything that's in requirements.txt and,
since `black` isn't available on Python 2.7, it was failing. See
https://readthedocs.org/projects/mongoengine-odm/builds/9371765/.

Refs #2105
2019-07-15 09:32:26 +02:00
Nuno Rodrigues
290d9df3eb Set default database when using mongomock.
When passing host `mongomock://localhost/some-default-database` to `connect` the default database was `"test"` instead of `"some-default-database"`.

Fixes: #2130
2019-07-12 13:33:19 +01:00
Stefan Wójcik
aa76ccdd25
Fix Document._object_key (#2125)
Previous implementation of `Document._object_key` was *pretending* to work on
MongoEngine-level fields (e.g. using "pk" instead of "_id" and separating
nested field parts by "__" instead of "."), but then it was also attempting to
transform field names from the `shard_key` into DB-level fields.

This, expectedly, didn't really work well. Most of the test cases added in this
commit were failing prior to the code fixes.
2019-07-09 12:08:26 +02:00
Andreas Doll
abe8070c36 Document register_connection arguments in correct order (#2121)
Put the documentation of the parameter 'name' of the function
'register_connection' in order of appearance in the function signature.
2019-07-08 14:16:09 +02:00
Bastien Gérard
2d28c258fd
Merge pull request #2115 from bagerard/use_globals_in_travis_yml
Use global constants for mongo/pymongo in travis.yml
2019-07-03 11:47:35 +02:00
Stefan Wójcik
1338839b52
Update changelog.rst 2019-07-03 11:18:56 +02:00
Stefan Wojcik
058203a0ec More changelog tweaks [ci skip]
The main change is that we're now using double backticks (``), which are the
correct way to format code in an RST file.
2019-07-03 11:12:25 +02:00
Stefan Wojcik
8fdf664968 Changelog tweaks [ci skip] 2019-07-03 11:08:51 +02:00
Stefan Wójcik
50555ec73e
Better management of the automatic "_cls" field (#2112)
* Rename BaseQuerySet._initial_query to BaseQuerySet._cls_query

This new name more accurately reflects the purpose of the dict. It is either
empty for documents that don't use inheritance or it contains a `{"_cls": ...}`
shape query. There was nothing "initial" about it per se.

* Drop read_preference as a kwarg on BaseQuerySet.__call__/filter

It was a poor design choice to offer two different ways to do the same thing:
1. `SomeDoc.objects(foo=bar, bar=baz).read_preference(...)`
2. `SomeDoc.objects(foo=bar, bar=baz, read_preference=...)`

Option 1 is good because it's immediately obvious which part defines the query
to be used and which part defines the read preference.

Option 2 is bad because you don't immediately know whether `read_preference`
is a query kwarg or a reserved keyword with some special behavior. If you
wanted to be particularly cruel, you could even write
`SomeDoc.objects(foo=bar, read_preference=..., bar=baz)`.

THIS IS A BREAKING CHANGE. From now on you have to use the
`BaseQuerySet.read_preference(...)` method.

* Add a BaseQuerySet.clear_cls_query method + get rid of the class_check kwarg

This is similar to what the previous commit did to read preference except that
in this case we were still missing a `BaseQuerySet` method for clearing the
`_cls` query.

Now, instead of the undocumented, untested, and confusing interface:
    `ParentDoc.objects(foo=bar, bar=baz, class_check=False)`
We do:
    `ParentDoc.objects(foo=bar, bar=baz).clear_cls_query()`
2019-07-03 11:07:55 +02:00
Bastien Gérard
951a532a9f additional fix in travis/tox 2019-07-03 09:15:28 +02:00
Bastien Gérard
e940044603 fixes in .travis.yml and tox.ini based on PR review 2019-07-02 23:06:31 +02:00
Bastien Gérard
babfbb0fcd Merge branch 'master' of github.com:MongoEngine/mongoengine into use_globals_in_travis_yml 2019-07-02 22:57:03 +02:00
Stefan Wojcik
bbed312bdd Final changelog tweaks [ci skip] 2019-07-02 18:42:50 +02:00
Stefan Wojcik
b593764ded One more changelog tweak [ci skip] 2019-07-02 18:35:29 +02:00
Stefan Wojcik
483c840fc8 One more changelog tweak [ci skip] 2019-07-02 18:29:28 +02:00
Stefan Wojcik
de80f0ccff Clean up the changelog [ci skip]
Mostly making sure that code is formatted using backticks and that wording and
punctuation are consistent.
2019-07-02 18:28:04 +02:00
Stefan Wójcik
d0b87f7f82
Drop the deprecated "format" param from BaseQuerySet.explain (#2113)
That option was pretty useless. You can very easily do:
```
import pprint
(...)

plan = SomeDoc.objects(...).explain()
pprint.pformat(plan)
```
2019-07-01 10:18:47 +02:00
Bastien Gérard
bf32d3c39a Merge branch 'master' of github.com:MongoEngine/mongoengine into use_globals_in_travis_yml 2019-06-30 21:11:42 +02:00
Bastien Gérard
bc14f2cdaa
Merge pull request #2083 from bagerard/improve_travis_yml2
add pypy3 back to travis and improve python 3.7 integration
2019-06-30 21:11:00 +02:00
Bastien Gérard
06a21e038a Use global constants for mongo/pymongo in travis.yml 2019-06-30 21:04:23 +02:00
Bastien Gérard
4d5eba317e convert travis test on latest python/mongo/pymongo from python 3.6 to 3.7 2019-06-30 20:55:19 +02:00
Bastien Gérard
d37a30e083 improve doc (based on review) 2019-06-30 20:46:40 +02:00
Stefan Wójcik
9170eea784
Rename MongoEngineConnectionError to ConnectionFailure (#2111)
I originally changed the exception name from `ConnectionError` to
`MongoEngineConnectionError` in
b02904ee75,
inspired by landscape.io's package health report, which argued that
`ConnectionError` is already a built-in exception in Python 3 (which it is:
https://docs.python.org/3/library/exceptions.html#ConnectionError).

I do agree that we shouldn't override built-in exceptions. [0] That said, it’s
silly to add a "MongoEngine" prefix to any class within the `mongoengine`
module (and *especially* to *just one* exception class out of many). I've
decided to do what PyMongo does (
8855a510a8/pymongo/errors.py (L59))
and call this exception `ConnectionFailure`.

Note that this is a breaking change and people will need to rename
`MongoEngineConnectionError`s in their code to `ConnectionFailure`. Moreover,
if they use PyMongo's `ConnectionFailure` for anything, they'll need to take
    extra care to avoid conflicts, e.g. by using:
```
from mongoengine import ConnectionFailure as MongoEngineConnectionFailure
```

[0] Note that some popular packages still overwrite `ConnectionError`, e.g.
4983a9bde3/requests/exceptions.py (L32)
or
0be4d29206/redis/exceptions.py (L8)
2019-06-30 09:23:32 +02:00
Stefan Wojcik
2769967e1e Update the changelog [ci skip] 2019-06-27 17:41:29 +02:00
Stefan Wójcik
609f50d261
Fix the duplicate ListField max_length test (#2110)
This is a follow-up after #2107.

Not sure what happened here, but in
87194856ec
I committed a copy-paste of the same test instead of a test validating the
max_length behavior along with a "set" operator.
2019-06-27 16:45:31 +02:00
Stefan Wójcik
82f0eb1cbc
Add a max_length param to the ListField (#2107)
This is similar to the `max_length` param of a `StringField`.

Sometimes you don't want your lists to be able to grow indefinitely.
2019-06-27 15:07:02 +02:00
Stefan Wójcik
b47669403b
Format the codebase using Black (#2109)
This commit:
1. Formats all of our existing code using `black`.
2. Adds a note about using `black` to `CONTRIBUTING.rst`.
3. Runs `black --check` as part of CI (failing builds that aren't properly formatted).
2019-06-27 13:05:54 +02:00
Stefan Wojcik
91899acfe5 Clarify unack'd write concern not returning the deleted count [ci skip] 2019-06-26 15:14:43 +02:00
Stefan Wójcik
ffedd33101
Drop support for positional arguments when instantiating a document (#2103)
For example, if you had the following class:
```
class Person(Document):
    name = StringField()
    age = IntField()
```

You could instantiate an object of such class by doing one of the following:
1. `new_person = Person('Tom', 30)`
2. `new_person = Person('Tom', age=30)`
3. `new_person = Person(name='Tom', age=30)`

From now on, only option (3) is allowed.

Supporting positional arguments may sound like a reasonable idea in this
heavily simplified example, but in real life it's almost never what you want
(especially if you use inheritance in your document definitions) and it may
lead to ugly bugs. We should not rely on the *order* of fields to match a given
value to a given name.

This also helps us simplify the code e.g. by dropping the confusing (and
undocumented) `BaseDocument._auto_id_field` attribute.
2019-06-26 11:31:11 +02:00
Bastien Gérard
c9ed930606 Add a documentation page for validation 2019-06-25 22:31:48 +02:00
Stefan Wojcik
af292b0ec2 Bump version to v0.18.2 2019-06-25 16:52:31 +02:00
Stefan Wojcik
1ead7f9b2b Add changelog entries for v0.18.2 2019-06-25 16:51:56 +02:00
Stefan Wojcik
5c91877b69 Fix the Travis deployment condition
See https://github.com/MongoEngine/mongoengine/issues/2104 for details.

For now I'm hardcoding `$MONGODB = 3.4.17` just to get a release out there,
but we should probably use the globals going forward. Will do that in
a follow-up commit once I get the `travis-conditions` gem up and running and
hence can test `.travis.yml` changes without deploying.
2019-06-25 16:48:51 +02:00
Stefan Wojcik
e57d834a0d Fix automated tests for py3 2019-06-25 12:41:59 +02:00
Stefan Wojcik
0578cdb62e Cleaner loop using itertools.count() 2019-06-25 11:41:27 +02:00
Stefan Wojcik
b661afba01 Use set comprehensions for existing_fields & existing_db_fields 2019-06-25 11:34:31 +02:00
Bastien Gérard
b1002dd4f9
Merge pull request #2097 from bagerard/remove_deprecated_pymongo_methods
remove pymongo deprecated methods: find_and_modify & remove
2019-06-24 22:03:58 +02:00
Stefan Wojcik
8e69008699 Fill in the PR # in the changelog [ci skip] 2019-06-24 16:00:21 +02:00
Stefan Wojcik
f45552f8f8 Drop support for positional arguments when instantiating a document
For example, if you had the following class:
```
class Person(Document):
    name = StringField()
    age = IntField()
```

You could instantiate an object of such class by doing one of the following:
1. `new_person = Person('Tom', 30)`
2. `new_person = Person('Tom', age=30)`
3. `new_person = Person(name='Tom', age=30)`

From now on, only option (3) is allowed.

Supporting positional arguments may sound like a reasonable idea in this
heavily simplified example, but in real life it's almost never what you want
(especially if you use inheritance in your document definitions) and it may
lead to ugly bugs. We should not rely on the *order* of fields to match a given
value to a given name.

This also helps us simplify the code e.g. by dropping the confusing (and
undocumented) `BaseDocument._auto_id_field` attribute.
2019-06-24 15:44:35 +02:00
Stefan Wojcik
a4fe091a51 Cleaner code & comments in BaseField.__set__ 2019-06-21 13:51:53 +02:00
Stefan Wojcik
216217e2c6 Datastructures comments: fix typos and tweak formatting [ci skip] 2019-06-21 13:48:24 +02:00
Stefan Wojcik
799775b3a7 Slightly cleaner docstring of BaseQuerySet.no_sub_classes [ci skip] 2019-06-20 12:18:58 +02:00
Stefan Wójcik
ae0384df29
Improve Document.meta.shard_key docs (#2099)
This closes #2096. Previous documentation of the shard_key meta attribute was
missing the crucial point that it really only matters if your collection is
sharded over a compound index.
2019-06-20 11:25:51 +02:00
Bastien Gérard
8f57279dc7 remove pymongo deprecated methods: find_and_modify & remove 2019-06-19 23:04:23 +02:00
Bastien Gérard
e8dbd12f22
Merge pull request #2091 from bagerard/release_0_18_1
Bump version number and update changelog for 0.18.1
2019-06-18 22:56:57 +02:00
Bastien Gérard
ca230d28b4 fix typo in changelog 2019-06-18 22:18:10 +02:00
Bastien Gérard
c96065b187 Merge branch 'master' of github.com:MongoEngine/mongoengine into release_0_18_1 2019-06-18 22:17:06 +02:00
Bastien Gérard
2abcf4764d minor fixes based on review of #2082 2019-06-18 22:15:53 +02:00
Dmitry Voronenkov
6a4c342e45 Supported updates of an array by negative index 2019-06-18 19:45:37 +03:00
Stefan Wójcik
bb0b1e88ef
Split up custom PK field tests (#2095)
This more closely aligns with the rule that a single tests should test one
thing and one thing only. Previous code tested like 4 different things in a
single test and was hard to follow.
2019-06-18 15:43:46 +02:00
Bastien Gérard
63c9135184 Bump version number and update changelog for 0.18.1 2019-06-17 22:36:54 +02:00
Bastien Gérard
7fac0ef961
Merge pull request #2082 from divomen/v0.18.0_fix
Fix a big issue when determine if there is a new document
2019-06-17 22:30:18 +02:00
Bastien Gérard
5a2e268160 Add test case to prevent regression 2019-06-17 22:19:41 +02:00
Stefan Wojcik
a4e4e8f440 Tweaks to the QuerySet.order_by docstring 2019-06-17 17:28:41 +02:00
Stefan Wojcik
b62ce947a6 Cleaner mongoengine.connection.__all__ 2019-06-17 15:42:15 +02:00
Stefan Wojcik
9538662262 Slightly cleaner connection code 2019-06-17 15:34:11 +02:00
Stefan Wojcik
09d7ae4f80 More BaseDocument.__init__ documentation tweaks 2019-06-17 14:52:26 +02:00
Stefan Wojcik
d7ded366c7 Document params expected by BaseDocument.__init__ [ci skip] 2019-06-17 14:37:14 +02:00
Stefan Wójcik
09c77973a0
Clean up how _changed_fields are set in BaseDocument._from_son (#2090) 2019-06-17 13:41:02 +02:00
Stefan Wojcik
22f3c70234 Fix PyMongo dependency in the readme [ci skip] 2019-06-17 09:41:41 +02:00
Stefan Wojcik
6527b1386f Benchmarks: Python 3 tweaks + more consistent testing of small vs big docs 2019-06-17 09:31:51 +02:00
Bastien Gérard
baabf97acd Merge branch 'master' of github.com:MongoEngine/mongoengine into v0.18.0_fix 2019-06-16 10:52:44 +02:00
Bastien Gérard
97005aca66 set dist as xenial to avoid relying on flaky travis default dist 2019-06-15 13:49:37 +02:00
Bastien Gérard
6e8ea50c19 "added another aggregation test"
This reverts commit 4c31193b82ca665fe27b1ddce3cbacbb2b7fcd32.
2019-06-14 21:04:02 +02:00
Stefan Wojcik
1fcd706e11 Clearer docstring of Document._get_collection [ci skip] 2019-06-14 14:57:12 +02:00
Stefan Wojcik
008bb19b0b Add a test covering basic Document operations
It covers operations such as:
1. Document initialization.
2. Accessing/setting attributes on a Document instance.
3. Serializing a Document instance (via `to_mongo`).
4. Deserializing a Document instance (via `_from_son`).
5. Serializing + saving a Document in the database (via `save`).
5. Loading a Document from the database + deserializing (via `Doc.objects[0]`).

And it does so for both basic flat documents and more complex nested docs.
2019-06-14 11:59:41 +02:00
Stefan Wojcik
023acab779 Clean up benchmark.py and move it to benchmarks/test_inserts.py
1. Removes the cascade=save tests. It's not an option I'd recommend using AND
   it primarily matters if you have any reference fields in your document,
   which is not the case in this script.
2. Uses PyMongo-v3.x-style write concern.
3. Removes an old docstring describing some random benchmark run from the past.
4. Removes unused parts of the code.

I'll add more tests to the "benchmarks/" directory in future commits.
2019-06-14 11:59:41 +02:00
Bastien Gérard
68e8584520 add pypy3 back to travis and improve python 3.7 integration 2019-06-13 23:29:02 +02:00
Bastien Gérard
5d120ebca0
Merge pull request #2058 from bagerard/improve_travis_yml
Improve travis yml + add python3.7 to travis
2019-06-13 23:20:15 +02:00
Bastien Gérard
f91b89f723 remove dist:xenial as it recently became the default in travis 2019-06-13 23:07:25 +02:00
Bastien Gérard
1181b75e16 clean travis.yml 2019-06-13 22:50:19 +02:00
Bastien Gérard
5f00b4f923 refactor travis - mongo install and added python3.7 2019-06-13 22:50:19 +02:00
Bastien Gérard
4c31193b82 Revert "added another aggregation test"
This reverts commit d7285d43dd2be65a0b5b0a60292ba085788e86c7.
2019-06-13 20:53:56 +02:00
Dmitry Voronenkov
17fc9d1886 Fix a big issue when determine if there is a new document or we need to update.
With this issue all fields were update always (not only modified fields)
2019-06-13 19:58:44 +03:00
Bastien Gérard
d7285d43dd added another aggregation test 2019-06-12 23:54:20 +02:00
Stefan Wojcik
aa8a991d20 Try a different deployment condition
This time my attempt is based on the output found in another job that didn't
trigger a deployment: https://travis-ci.org/MongoEngine/mongoengine/jobs/544664203

```
/home/travis/.travis/job_stages: line 660: expected `)'
/home/travis/.travis/job_stages: line 660: syntax error near `AND'
/home/travis/.travis/job_stages: line 660: `  if [[ ($TRAVIS_REPO_SLUG = "MongoEngine/mongoengine") && ($TRAVIS_PYTHON_VERSION = 2.7) && ($PYMONGO = 3.x AND $MONGODB = 3.4) && ("$TRAVIS_TAG" != "") ]]; then'
```

See 80ca6360c1f3ea073e3fcb65070ded0558514ffa and
40ba51ac43591cbc8547f8d2db6c7ac6d1d97cee for my previous attempts.
2019-06-12 12:19:36 +02:00
Stefan Wojcik
40ba51ac43 Try a different deployment condition
The previous one was a verbatim copy-paste of what TravisCI's Support suggested
to me, but sadly it didn't work. See
https://travis-ci.org/MongoEngine/mongoengine/jobs/544655132. That build
should've triggered a deployment.

This time I'm trying a different syntax, primarily influenced by
https://docs.travis-ci.com/user/conditions-v1#boolean-operators.
2019-06-12 12:08:11 +02:00
Stefan Wojcik
d20430a778 Bump up waiting for MongoDB from 15s to 20s
I've noticed that `mongo --eval 'db.version()'` has been failing fairly
regularly in the last few weeks. Hopefully that extra 5s is enough.
2019-06-12 11:57:25 +02:00
Stefan Wojcik
f08f749cd9 Bump version to v0.18.0 2019-06-12 11:47:31 +02:00
Stefan Wojcik
a6c04f4f9a Finalize the v0.18.0 changelog [ci skip] 2019-06-12 11:38:58 +02:00
Stefan Wojcik
15b6c1590f Add extra context to the BaseDocument.validate docstring 2019-06-12 11:37:08 +02:00
Bastien Gérard
4a8985278d Document inherited members for the Document, EmbeddedDocument, DynamicDocument, and DynamicEmbeddedDocument (#2040) 2019-06-12 11:33:56 +02:00
Stefan Wojcik
996618a495 Fix wording of an exception message in QuerySet.insert 2019-06-12 08:29:59 +02:00
erdenezul
1f02d5fbbd
Merge pull request #1570 from erdenezul/remove_save_embedded
EmbeddedDocument should not have save method #1552
2019-06-11 16:15:53 +02:00
Erdenezul Batmunkh
c58b9f00f0 Add changelog 2019-06-11 15:53:50 +02:00
Stefan Wojcik
f131b18cbe Make test_update_shard_key_routing more resilient 2019-06-11 15:50:22 +02:00
Stefan Wojcik
118a998138 Classify the QuerySet.aggregate change as a bugfix [ci skip] 2019-06-11 15:09:16 +02:00
Erdenezul Batmunkh
7ad6f036e7 Remove test 2019-06-11 13:16:33 +02:00
Erdenezul Batmunkh
1d29b824a8 Remove save method from test 2019-06-11 12:52:29 +02:00
Erdenezul Batmunkh
3caf2dce28 Merge branch 'master' into remove_save_embedded 2019-06-11 12:41:11 +02:00
Bastien Gérard
1fc5b954f2 fix typo in changelog 2019-06-10 22:38:37 +02:00
Stefan Wojcik
31d99c0bd2 Cleaner wording in the dev changelog 2019-06-10 11:26:47 +02:00
Bastien Gérard
0ac59c67ea
Merge pull request #2068 from bagerard/fix_connection_auth_same_host
Fix connection issue when using different authentication in different dbs
2019-06-07 21:08:26 +02:00
Stefan Wojcik
8e8c74c621 Drop the unused mongodb_version attribute in IndexesTest 2019-06-07 12:35:38 +02:00
Stefan Wojcik
f996f3df74 Cleaner test_hint 2019-06-07 12:34:32 +02:00
Stefan Wojcik
9499c97e18 Clean up the .install_mongodb_on_travis.sh script
This is a leftover from #2066. Since we no longer install MongoDB versions
v2.6 – v3.2, we no longer need this code.
2019-06-07 12:16:32 +02:00
erdenezul
c1c81fc07b
Merge pull request #2070 from bagerard/improve_doc_of_custom_field_validation
Document the custom field validation feature
2019-06-05 22:30:40 +02:00
erdenezul
072e86a2f0
Merge pull request #2069 from bagerard/some_refactoring
minor refactoring and additional of tests
2019-06-05 22:30:09 +02:00
Bastien Gérard
70d6e763b0 Document the custom field validation feature 2019-06-05 22:23:54 +02:00
Bastien Gérard
15f4d4fee6 fix tests for diff mongo vers 2019-06-05 21:51:21 +02:00
Bastien Gérard
82e28dec43 improved string operation code 2019-06-04 23:17:10 +02:00
Bastien Gérard
b407c0e6c6 add test for shard key routing (ported from https://github.com/closeio/mongoengine/commit/43f35f5) 2019-06-04 23:17:10 +02:00
Bastien Gérard
27ea01ee05 refactored datetime to_mongo, separating parsing from str + added test 2019-06-04 23:16:26 +02:00
Bastien Gérard
7ed5829b2c Add test on datetime field - parse datetime as str 2019-06-04 23:16:26 +02:00
Bastien Gérard
5bf1dd55b1
Update mongomock example
Improved the mongomock example as reported in #2067 
 
Fixes #2067
2019-06-04 22:56:52 +02:00
Bastien Gérard
36aebffcc0 update changelog 2019-06-04 22:39:44 +02:00
Bastien Gérard
84c42ed58c Add tests 2019-06-04 22:35:42 +02:00
Bastien Gérard
9634e44343 Fix the issue that the same MongoClient gets re-used in case we connect to 2 databases on the same host (problematic when different users authenticate) 2019-06-04 22:12:46 +02:00
Bastien Gérard
048a045966
Update connection/multiple databases docs
I observed that many people were confused by this so I thought I'd make the multiple databases example more explicit
2019-06-04 21:47:28 +02:00
Bastien Gérard
a18c8c0eb4
Merge pull request #2049 from bagerard/save_to_mongo_call_in_save
Improve perf of Document.save
2019-06-01 15:00:44 +02:00
Bastien Gérard
5fb0f46e3f fix changelog (py37 not yet in travis) 2019-06-01 11:16:29 +02:00
Bastien Gérard
962997ed16 fix flaky test due to signal receiver garbage collection 2019-06-01 11:13:28 +02:00
Bastien Gérard
daca0ebc14 update changelog 2019-06-01 11:13:28 +02:00
Bastien Gérard
9ae8fe7c2d Improve perf of Doc.save by preventing a full to_mongo() call just to get the created variable 2019-06-01 11:13:28 +02:00
Bastien Gérard
1907133f99
Merge pull request #2050 from bagerard/change_custom_field_validation_raise
custom field validator is now expected to raise a ValidationError
2019-06-01 10:45:43 +02:00
Stefan Wójcik
4334955e39
Update the test matrix to reflect what's supported in 2019 (#2066)
Previously, we were running the test suite for several combinations of MongoDB,
Python, and PyMongo:
- PyPy, MongoDB v2.6, PyMongo v3.x (which really means v3.6.1 at the moment)
- Python v2.7, MongoDB v2.6, PyMongo v3.x
- Python v3.5, MongoDB v2.6, PyMongo v3.x
- Python v3.6, MongoDB v2.6, PyMongo v3.x
- Python v2.7, MongoDB v3.0, PyMongo v3.5.0
- Python v3.6, MongoDB v3.0, PyMongo v3.5.0
- Python v3.5, MongoDB v3.2, PyMongo v3.x
- Python v3.6, MongoDB v3.2, PyMongo v3.x
- Python v3.6, MongoDB v3.4, PyMongo v3.x
- Python v3.6, MongoDB v3.6, PyMongo v3.x

There were a couple issues with this setup:
1. MongoDB v2.6 – v3.2 have reached their End of Life already (v2.6 almost 3
   years ago!). See the "MongoDB Server" section on
   https://www.mongodb.com/support-policy.
2. We were only testing two recent-ish PyMongo versions (v3.5.0 & v3.6.1).
   We were not testing the oldest actively supported MongoDB/PyMongo/Python
   setup.

This PR updates the test matrix so that these problems are solved. For the
sake of simplicity, it does not yet attempt to cover MongoDB v4.0:
- PyPy, MongoDB v3.4, PyMongo v3.x (aka v3.6.1 at the moment)
- Python v2.7, MongoDB v3.4, PyMongo v3.x
- Python v3.5, MongoDB v3.4, PyMongo v3.x
- Python v3.6, MongoDB v3.4, PyMongo v3.x
- Python v2.7, MongoDB v3.4, PyMongo v3.4
- Python v3.6, MongoDB v3.6, PyMongo v3.x
2019-05-31 11:01:15 +02:00
Bastien Gérard
f00c9dc4d6 Fix flake8 import error 2019-05-28 09:26:07 +02:00
Bastien Gérard
7d0687ec73 custom field validator is now expected to raise a ValidationError (drop support for returning True/False) 2019-05-28 09:26:07 +02:00
Bastien Gérard
da3773bfe8
Merge pull request #2063 from bagerard/improve_test
Improve minor things in the tests
2019-05-26 22:33:40 +02:00
Bastien Gérard
6e1c132ee8 Improve minor things in the tests 2019-05-26 22:17:58 +02:00
Bastien Gérard
24ba35d76f
Merge pull request #2062 from george-pearson/deprecation_warning_pymongo
Use update_one instead of deprecated update #1899
2019-05-26 21:20:56 +02:00
George Pearson
64b63e9d52 Use update_one instead of deprecated update #1899 2019-05-26 17:29:23 +01:00
erdenezul
7848a82a1c
Merge pull request #2032 from bagerard/remove_pymongo2_support_dead_code
remove dead code (related to pymongo2)
2019-05-25 14:43:20 +02:00
Bastien Gérard
6a843cc8b2 Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_pymongo2_support_dead_code 2019-05-23 21:06:15 +02:00
Bastien Gérard
ecdb0785a4 Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_pymongo2_support_dead_code 2019-05-23 21:04:58 +02:00
erdenezul
9a55caed75
Merge pull request #2056 from bagerard/support_mongo36
Add support for MongoDB 3.6 and Python3.7 in travis
2019-05-18 17:06:48 +02:00
Bastien Gérard
2e01eb87db Add support for MongoDB 3.6 and Python3.7 in travis 2019-05-18 14:29:42 +02:00
erdenezul
597b962ad5
Merge pull request #2055 from bagerard/improve_test_cov
Improve test cov
2019-05-18 12:40:20 +02:00
Bastien Gérard
7531f533e0
Merge pull request #2054 from abarto/add-nin-support-transform
Add support for '$nin' when transforming a 'pull' update query.
2019-05-18 11:14:32 +02:00
Agustin Barto
6b9d71554e Add integration tests 2019-05-17 17:23:52 -03:00
Bastien Gérard
bb1089e03d Improve coverage in fields test 2019-05-17 22:16:08 +02:00
Bastien Gérard
c82f0c937d more work on coverage 2019-05-17 22:04:28 +02:00
Bastien Gérard
00d2fd685a more test cov 2019-05-17 22:04:28 +02:00
Bastien Gérard
f28e1b8c90 improve coverage of lazy ref field 2019-05-17 22:04:28 +02:00
Agustin Barto
2b17985a11 Uncomment tests. 2019-05-17 13:55:00 -03:00
Agustin Barto
b392e3102e Add support to transform. Add pull tests for and . 2019-05-17 13:41:02 -03:00
Bastien Gérard
58b0b18ddd
Merge pull request #2053 from bagerard/Fix_travis_incomp_tox_virtualenv
Fix Incompatibility btw recent tox version and virtualenv version
2019-05-16 23:15:09 +02:00
Bastien Gérard
6a9ef319d0 Fix Incompatibility btw recent tox version and virtualenv version 2019-05-16 23:01:43 +02:00
Bastien Gérard
cf38ef70cb Remove more code related to supporting pymongo2 2019-05-15 22:23:35 +02:00
Bastien Gérard
ac64ade10f remove dead code (related to pymongo2) + minor cleaning 2019-05-15 21:54:47 +02:00
erdenezul
ee85af34d8
Merge pull request #2043 from bagerard/fix_write_concern_in_save
Fix default write concern on save call that was overwriting connection WC
2019-05-15 15:26:50 +02:00
Erdenezul Batmunkh
9d53ad53e5 Remove save and reload from embeddeddocument 2019-05-10 17:33:59 +02:00
Bastien Gérard
9cdc3ebee6 Fix default write concern on save call that was overwriting connection wc 2019-05-05 23:37:12 +02:00
erdenezul
14a5e05d64
Merge pull request #2042 from bagerard/fix_querying_embedded_subcls
Fix querying embeddedDoc sub classes
2019-05-04 17:10:23 +02:00
Bastien Gérard
f7b7d0f79e Improve tests for querying list(embedded) when using inheritance 2019-05-03 21:59:48 +02:00
Bastien Gérard
d98f36ceff Add test for querying on fields of list(EmbeddedDocument) (with inheritance on the EmbededDoc) 2019-05-02 00:08:16 +02:00
Bastien Gérard
abfabc30c9 Fix querying on (Generic)EmbeddedDocument subclasses fields 2019-05-01 23:23:19 +02:00
erdenezul
c1aff7a248
Merge pull request #2038 from bagerard/disconnect
Fix connect/disconnect functions
2019-04-30 14:08:55 +02:00
Bastien Gérard
e44f71eeb1 updated changelog 2019-04-25 22:31:05 +02:00
Bastien Gérard
cb578c84e2 Merge branch 'master' of github.com:MongoEngine/mongoengine into disconnect 2019-04-25 22:15:48 +02:00
Bastien Gérard
565e1dc0ed minor improvements 2019-04-25 22:11:43 +02:00
Bastien Gérard
b1e28d02f7 Improve connect/disconnect
- document disconnect + sample of usage
- add more test cases to prevent github issues regressions
2019-04-24 22:44:07 +02:00
Bastien Gérard
d1467c2f73 Fix connect/disconnect functions
- expose disconnect
- disconnect cleans _connection_settings
- disconnect cleans cached collection in Document._collection
- re-connecting with the same alias raise an error (must call disconnect in between)
2019-04-24 22:41:56 +02:00
Bastien Gérard
c439150431
Merge pull request #2031 from yandrieiev/fail_fast_when_invalid_db_name
Fail fast when db name is invalid
2019-04-10 22:50:29 +02:00
Bastien Gérard
9bb3dfd639 updated changelog for recent commits + improve tests 2019-04-07 23:05:55 +02:00
Bastien Gérard
4caa58b9ec
Merge pull request #2029 from nsuthar0914/patch-1
Fix limit usage in aggregate
2019-04-07 22:32:31 +02:00
Yurii Andrieiev
b5213097e8 Fail fast when db name is invalid
Without this commit save operation on first document would fail instead of immediate failure upon connection attempt. Such later failure is much less obvious.
2019-04-07 23:21:12 +03:00
Neeraj Suthar
61081651e4 reinsert fix; add comments, reference 2019-04-06 17:42:14 +05:30
Neeraj Suthar
4ccfdf051d remove fix; add testcases 2019-04-06 17:23:02 +05:30
Neeraj
9f2a9d9cda
Fix limit usage in aggregate
As per https://stackoverflow.com/a/24161461
2019-04-03 19:09:45 +05:30
Bastien Gérard
827de76345
Merge pull request #2007 from GVRV/feature/fix_multiple_dereference_calls
Fix Multiple Redundant Dereference Calls
2019-03-30 20:39:31 +01:00
Gaurav Dadhania
fdcaca42ae Do not keep calling _dereference on values if it has already been dereferenced. 2019-03-25 09:43:42 +05:30
erdenezul
0744892244
Merge pull request #2022 from bagerard/bump_pymongo_version_requirement
Bump the required version of pymongo to >=3.5
2019-03-19 09:01:18 +08:00
erdenezul
b70ffc69df
Merge branch 'master' into bump_pymongo_version_requirement 2019-03-19 08:50:21 +08:00
Bastien Gérard
73b12cc32f
Merge pull request #2021 from pauloAmaral/fix_indices_sortedlist_embedded_document_list
Generate Unique Indices for SortedListField and EmbeddedDocumentListField
2019-03-18 18:23:28 +01:00
Paulo Amaral
ba6a37f315 Generate Unique Indices for SortedListField and EmbeddedDocumentListFields 2019-03-18 11:32:53 +00:00
Bastien Gérard
6f8be8c8ac document change in changelog 2019-03-17 22:11:01 +01:00
Bastien Gérard
68497542b3 Bump the required version of pymongo to >=3.5 2019-03-17 22:04:19 +01:00
Bastien Gérard
3d762fed10
Merge pull request #2016 from lalala223/master
Update querying.rst
2019-03-16 20:27:48 +01:00
lalala223
48b849c031
Update querying.rst
Fix the 'not' operator error example.
2019-03-13 17:50:54 +08:00
Bastien Gérard
88c4aa2d87
Merge pull request #2014 from MongoEngine/draft_v017
Bump version 0.17.0
2019-03-11 22:02:41 +01:00
Erdenezul Batmunkh
fb8c0d8fe3 Merge branch 'master' into draft_v017 2019-03-10 22:22:29 +08:00
erdenezul
1a863725d1
Merge pull request #2011 from bagerard/fix_batch_size_qs_clone
Fix queryset batch_size that wasn't copied to cloned queryset
2019-03-10 21:22:46 +08:00
Erdenezul Batmunkh
7b4245c91c Bump version 0.17.0 2019-03-10 21:16:58 +08:00
Bastien Gérard
9bd0d6b99d update changelog 2019-03-04 23:05:22 +01:00
Bastien Gérard
b640c766db Fix queryset batch_size that wasn't copied to cloned queryset 2019-03-04 23:01:12 +01:00
erdenezul
50ffa8014e
Merge pull request #2008 from bagerard/refactor_deprecated_pymongo_test
refactored deprecated pymongo methods in tests
2019-03-03 08:06:48 +08:00
Bastien Gérard
7ef688b256 Added a test for push in DictField (relates to #1679) 2019-03-02 22:05:23 +01:00
Bastien Gérard
b4fe0b35e4 Merge branch 'master' of github.com:MongoEngine/mongoengine into refactor_deprecated_pymongo_test 2019-03-02 21:48:41 +01:00
erdenezul
a2cbbdf819
Merge pull request #2009 from bagerard/mongodb_34
Add support for Mongodb 34 (tests/travis)
2019-03-02 08:24:59 +08:00
Bastien Gérard
35b7efe3f4 refactored deprecated pymongo methods in tests
- remove/count/add_user/insert
- added pymongo_support
2019-03-02 00:28:38 +01:00
Bastien Gérard
7cea2a768f Fix recent flaky test for python 3.6 2019-02-26 22:57:45 +01:00
Bastien Gérard
7247b9b68e additional fixes to support Mongo3.4 2019-02-26 21:38:23 +01:00
Bastien Gérard
dca837b843 Add suport for Mongo 3.4 (travis, fix tests) 2019-02-26 21:38:23 +01:00
Bastien Gérard
c60c2ee8d0 fix minor styling issue in tests 2019-02-25 22:33:36 +01:00
Bastien Gérard
3cdb5b5db2 fix poor assert's in tests 2019-02-25 22:29:44 +01:00
Bastien Gérard
b9cc8a4ca9 refactor more field tests into submodules #1983 2019-02-24 10:24:12 +01:00
Bastien Gérard
28606e9985 refactor fields tests (float, int, lazyref, long, url) #1983 2019-02-24 10:24:12 +01:00
Bastien Gérard
5bbe782812 fix deprecated call to pymongo save() in tests 2019-02-23 22:37:32 +01:00
erdenezul
d65861cdf7
Merge pull request #2004 from bagerard/fix_iteritems_itervalues_23_compat
refactored iteritems/itervalues to improve 2/3 compat
2019-02-23 22:46:21 +08:00
Bastien Gérard
c8df3fd2a7 fix conflict 2019-02-21 21:27:08 +01:00
Bastien Gérard
6cfe6652a3
Merge pull request #2001 from bagerard/separate_date_fields_tests_into_submodules
Separate fields tests into separate modules (date/datetime/complexdt)
2019-02-19 20:57:15 +01:00
erdenezul
6b711da69d
Merge pull request #1998 from bagerard/Fix_update_full_result_documentation
Fix documentation of Queryset.update regarding full_result #1995
2019-02-19 17:16:42 +08:00
erdenezul
9b02867293
Merge branch 'master' into remove_save_embedded 2019-02-19 17:15:14 +08:00
erdenezul
595cb99b2d
Merge pull request #2005 from bagerard/add_deprecation_warn_embeddedDoc_save
Add DeprecationWarning for EmbeddedDocument.save & .reload
2019-02-19 17:13:57 +08:00
Bastien Gérard
f0a3445250 minor fix for import order 2019-02-18 22:22:20 +01:00
Bastien Gérard
6d353dae1e refactored iteritems/itervalues to improve 2/3 compat #2003 2019-02-18 22:13:05 +01:00
Bastien Gérard
57a38282a9 Add DeprecationWarning for EmbeddedDocument.save & .reload - those will be removed soon 2019-02-18 22:03:03 +01:00
erdenezul
db47604865
Merge pull request #1999 from bagerard/fix_documentation_dynamicdoc_underscore_field_undeclared
Improve DynamicDocument documentation regarding fields with underscore
2019-02-18 09:33:39 +08:00
erdenezul
2a121fe202
Merge branch 'master' into Fix_update_full_result_documentation 2019-02-18 09:31:00 +08:00
erdenezul
36baff0d7f
Merge pull request #2002 from bagerard/freyr_binaryfield_query_value_type
Fix Binaryfield query operator
2019-02-18 09:27:01 +08:00
Bastien Gérard
201f3008b1 Fix for py3 in binary field test 2019-02-17 22:56:56 +01:00
Bastien Gérard
f4873fee18 add additional test for #1976 2019-02-17 22:50:42 +01:00
Bastien Gérard
e02261be6d add test coverage for #1557 2019-02-17 22:22:29 +01:00
Bastien Gérard
2919e6765c separate test on binary field (#1983) 2019-02-17 21:45:05 +01:00
Bastien Gérard
b8fc4d0079 Merge branch 'master' of github.com:MongoEngine/mongoengine into freyr_binaryfield_query_value_type 2019-02-17 21:38:24 +01:00
Bastien Gérard
4a46f5f095 Separate fields tests into separate modules (date/datetime/complexdatetime)
relates to #1983
2019-02-17 21:32:46 +01:00
Bastien Gérard
3484ceabb8 Fix documentation regarding limitation with fields with underscore in their name in DynamicDocument 2019-02-17 09:05:27 +01:00
Bastien Gérard
cab659dce6 Fix documentation of Queryset.update regarding full_result #1995 2019-02-16 21:54:05 +01:00
Bastien Gérard
a657f29439
Merge pull request #1996 from hrmon/patch-1
Fix indentation in LazyReferenceField ref doc
2019-02-16 20:31:19 +01:00
Hamidreza
4c054bf316
Fix indentation in LazyReferenceField ref doc 2019-02-13 21:27:54 +03:30
erdenezul
dc7922c38b
Merge pull request #1976 from 3ddi/eddie/revert_hidden_fields
Bring back _cls and _id fields just as they return in pymongo
2019-02-13 14:25:12 +08:00
Eddie Linder
c6c68abfcc Update changelog and add another ut case 2019-01-11 19:23:47 +02:00
Bastien Gérard
6aacb0c898
Merge pull request #1982 from gbroques/add-long-field-to-docs
Add missing LongField to Fields list in Documentation
2018-12-29 23:28:02 +01:00
G Roques
e7000db491 Add LongField to Fields list 2018-12-29 13:08:39 -06:00
Bastien Gérard
fce994ea7f
Merge pull request #1975 from bagerard/dictfield-key-with-dollar-char__clone
PR CLONE - Dictfield key with dollar char  clone
2018-12-27 23:02:00 +01:00
erdenezul
6c6446765e
Merge pull request #1980 from bagerard/update_all_doc
minor update to .all() doc to make it clear that it return a queryset
2018-12-26 11:50:11 +08:00
Bastien Gérard
69a99c70c6 minor update to .all() doc to make it clear that it does not return a list but a queryset 2018-12-24 23:51:30 +01:00
Eddie Linder
56d9f7a8af Fix ut to pass constantly 2018-12-20 01:14:23 +02:00
Eddie Linder
363aefe399 Get rid of _get_as_pymongo wrapper altogether as we return raw output 2018-12-20 01:09:28 +02:00
Eddie Linder
7fd4f792ba Don't hide _cls field when using as_pymongo 2018-12-20 01:02:42 +02:00
Eddie Linder
6fbdde63d8 Fix id in test_only_after_count ut 2018-12-20 00:57:02 +02:00
Eddie Linder
b04dc90cdf Bring back _cls and _id fields just as they return in pymongo 2018-12-20 00:39:32 +02:00
Bastien Gérard
b525c91bd3 remove tags file that shouldnt be added to situkangsayur s commit 2018-12-19 20:33:37 +01:00
Hendri Karisma
a32c893078 in the mid DictField key can accept dollar sign 2018-12-19 20:24:28 +01:00
Bastien Gérard
2c6a744848
Merge pull request #1853 from bagerard/fix_baselist_marked_changed_bug
Fix BaseList/Dict bug with marked_as_changed getting called when operation fails
2018-12-19 20:16:57 +01:00
Bastien Gérard
4492874d08 Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_baselist_marked_changed_bug 2018-12-15 20:36:42 +01:00
erdenezul
d3a592e5bf
Merge pull request #1913 from bagerard/1453_mapfield_list_reference_field
Fix complex combinations of DictField(ListField(ReferenceField))
2018-12-13 09:16:19 +08:00
erdenezul
cab21b1b21
Merge pull request #1969 from buggyspace/master
Fix .only() working improperly after using .count() of the same instance of QuerySet
2018-12-12 19:25:31 +08:00
Tom Floyer
1319e422ea Updated docs/changelog.rst 2018-12-08 22:38:04 +03:00
Tom Floyer
c88ea40b57 Updated AUTHORS 2018-12-08 22:32:46 +03:00
Tom Floyer
3194a37fcb Reset cursor object after .count()
This change fixes incorrect result of .only() method of QuerySet
instance after using .count().
2018-12-08 22:16:57 +03:00
Tom Floyer
72ebaa52e9 Ensure that QuerySet.only() works correctly after QuerySet.count()
This test checks if .only() method of QuerySet instance works after
using .count() method.
2018-12-08 22:10:10 +03:00
erdenezul
0e00695fc7
Merge pull request #1968 from amir-almusawi/doc-typo
fixed small typographical error
2018-12-08 12:23:18 +08:00
amir-almusawi
48a691e722 fixed small typographical error 2018-12-07 22:08:05 -06:00
erdenezul
cf54d6d6f8
Merge pull request #1967 from erdenezul/bump_version_16_3
bump version 0.16.3
2018-12-06 21:56:31 +08:00
Erdenezul Batmunkh
a03fe234d0 bump version 0.16.3 2018-12-06 16:22:00 +08:00
erdenezul
d88d40cc08
Merge pull request #1966 from tjhall13/1965-fix-position-op-with-nested-list-in-embedded-document
Fix bug #1965 of $position and $push operators do not work with nested list
2018-12-06 14:02:46 +08:00
Trevor Hall
d3b4af116e Add fix to changelog.rst #1965 2018-12-05 23:35:42 -06:00
Trevor Hall
352b23331b Fix bug #1965 of $position and $push operators do not work with list in an EmbeddedDocument. Set key value to joined parts excluding the index at the end. Added test case 2018-12-05 20:18:48 -06:00
Bastien Gérard
bdd6041a5c
Merge pull request #1960 from bagerard/bump_version_0_16_2
bump version to 0.16.2
2018-11-21 13:45:13 +01:00
Bastien Gérard
1894003f8a bump version to 0.16.2 2018-11-21 12:45:16 +01:00
erdenezul
220513ae42
Merge pull request #1959 from bagerard/fix_write_concern_default_param
Fix but with save(write_concern=None) - introduced in 0.16.1
2018-11-21 09:15:00 +08:00
Bastien Gérard
fcbabbe357 Fix but with save(write_concern=None) - introduced in 0.16.1 2018-11-21 00:03:08 +01:00
erdenezul
3627969fce
Merge pull request #1952 from erdenezul/bump_v0.16.1
bump version 0.16.1
2018-11-14 17:23:10 +08:00
Erdenezul Batmunkh
8807c0dbef bump version 0.16.1 2018-11-14 17:10:17 +08:00
erdenezul
23cc9f6ff8
Merge pull request #1951 from bagerard/fix_cls_in_constructor
Regression bug fix - _cls not set in constructor
2018-11-14 17:02:17 +08:00
Bastien Gérard
e50799e9c4 Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_cls_in_constructor 2018-11-13 21:48:16 +01:00
Bastien Gérard
b92c4844eb
Merge pull request #1949 from bagerard/fix_delta_bug
Fix bug in _delta method - setting ListField to empty in DynamicDocument is faulty
2018-11-13 21:44:02 +01:00
Bastien Gérard
c306d42d08 Fix bug #1733 of _delta method (Issue with DynamicDocument and default value) + add test case 2018-11-13 20:57:41 +01:00
Bastien Gérard
e31558318e BugFix - _cls not set in constructor #1950 2018-11-13 20:52:39 +01:00
erdenezul
78a9420f26
Merge pull request #1944 from erdenezul/deprecation_warning_pymongo37
Use insert_one instead of deprecated one #1899
2018-11-13 19:40:22 +08:00
Erdenezul Batmunkh
b47c5b5bfc Adhere imports into existing one #1899 2018-11-12 09:53:39 +08:00
erdenezul
28a312accf
Merge pull request #1946 from erdenezul/refactor_write_concern
Refactor write_concern #1945
2018-11-09 09:30:20 +08:00
Erdenezul Batmunkh
611094e92e Refactor write_concern #1945 2018-11-09 09:20:55 +08:00
Erdenezul Batmunkh
2a8579a6a5 Update changelog #1899 2018-11-09 09:12:29 +08:00
Erdenezul Batmunkh
47577f2f47 Update existing document #1899 2018-11-09 01:44:30 +08:00
Erdenezul Batmunkh
34e3e45843 Use insert_one instead of deprecated one #1899 2018-11-08 23:47:12 +08:00
Bastien Gérard
364dc9ddfb
Merge pull request #1937 from bagerard/changelog
Bump version to 0.16.0 + update changelog
2018-11-05 09:38:58 +01:00
Bastien Gérard
23324f0f87 minor fix in ImageField docstring 2018-11-04 22:14:27 +01:00
Bastien Gérard
17fa9a3b77 bump version to 0.16.0 2018-11-03 21:48:12 +01:00
Bastien Gérard
424b3ca308 update changelog with changes since 0.15.3 2018-11-03 21:47:56 +01:00
Bastien Gérard
26e2fc8fd4
Merge pull request #1887 from bagerard/fix_changed_fields_issue_same_id_in_nested_doc2
Fix bug where an EmbeddedDocument with the same id as its parent would not be tracked for changes
2018-11-01 22:49:07 +01:00
Bastien Gérard
8e18484898 Add a comment on a suspicious line (#1322) 2018-11-01 22:28:12 +01:00
Bastien Gérard
354cfe0f9c
Merge pull request #1935 from bagerard/generic_ref_field_changed
Fix bug when doing modifications to generic-referenced document
2018-10-31 15:53:13 +01:00
Bastien Gérard
983474b2bd Ignore 2 more flake8 warnings (introduced in latest flake8 3.6.0 release) 2018-10-30 23:40:46 +01:00
Bastien Gérard
14d861bcbb Merge branch 'master' of https://github.com/MongoEngine/mongoengine into generic_ref_field_changed 2018-10-30 23:21:19 +01:00
Bastien Gérard
f6cd349a16 Fix bug when using GenericReferenceField, modifications to the referenced document are tracked in the parent #1934 2018-10-30 23:00:05 +01:00
Bastien Gérard
8e1c4dec87
Merge pull request #1846 from bagerard/fix_validator_of_binary_field
fix validator of BinaryField
2018-10-26 22:14:08 +02:00
erdenezul
18b47e4a73
Merge pull request #1932 from bagerard/improve_index_doc
minor improvement to index doc
2018-10-25 10:19:04 +08:00
erdenezul
4f157f50ed
Merge pull request #1931 from bagerard/fix_no_dereference_doc_asserts
fix some asserts in no_dereference doc
2018-10-25 10:15:33 +08:00
erdenezul
f44a2f4857
Merge pull request #1843 from terencehonles/add-__repr__-to-Q-and-QCombination
add __repr__ to Q and QCombination
2018-10-25 10:14:05 +08:00
Bastien Gérard
c685ace327 minor improvement to index doc 2018-10-23 23:55:55 +02:00
Bastien Gérard
f23b0faf41 fix some asserts in no_dereference doc 2018-10-23 22:10:34 +02:00
erdenezul
e0e2ca7ccd
Merge pull request #1930 from bagerard/improve_doc_indexes
Improve documentation for additional index options
2018-10-23 10:16:27 +08:00
Bastien Gérard
83fe7f7eef Document the index option and the fact that additional options gets forwarded to pymongo's create_index method 2018-10-22 21:54:46 +02:00
erdenezul
1feaa8f2e9
Merge pull request #1927 from possnfiffer/patch-1
Update README.rst
2018-10-22 16:44:36 +08:00
__ROLLER__
598d6bf4c5
Update README.rst
Added information on using pipenv to the Installation section
2018-10-21 12:47:23 -06:00
erdenezul
0afd5a40d6
Merge pull request #1923 from bagerard/update_document_ids_during_bulk_insert
Update the ids of the given documents during bulk insert
2018-10-17 19:03:11 +08:00
Bastien Gérard
26b70e9ed3 fix test 2018-10-16 22:55:55 +02:00
Bastien Gérard
a1a93a4bdd minor additions 2018-10-16 22:35:58 +02:00
Bastien Gérard
4939a7dd7c update input document ids during bulk insert #1919 2018-10-16 22:21:53 +02:00
Bastien Gérard
0fa6610fdb Fix complex combinations of DictField(ListField(ReferenceField)) (#1453) 2018-10-16 21:08:59 +02:00
erdenezul
b0148e7860
Merge pull request #1921 from bagerard/error_referencefield_of_abstract_classes
Fix a bug when using a ReferenceField(AbstractClass)
2018-10-16 07:55:52 +08:00
Bastien Gérard
59a06a242d Fix a bug when using a ReferenceField(AbstractClass) #1920 2018-10-15 22:32:11 +02:00
erdenezul
ffe902605d
Merge pull request #1918 from bagerard/improve_error_cant_subclass_document
Improve the error message that mentions that Document cant be subclassed
2018-10-15 11:18:13 +08:00
Bastien Gérard
556f7e85fc Improve the error message that mentions that Document cant be subclassed 2018-10-10 23:13:34 +02:00
erdenezul
45c86be402
Merge pull request #1916 from liuq/master
Bug fix: pre_save_post_validation could, in principle, modify the document
2018-10-10 17:12:39 +08:00
erdenezul
bf34f413de
Merge pull request #1912 from bagerard/fix_deprecation_warning_Hashable
Fix deprecation warning about Hashable in py3.7
2018-10-10 16:32:50 +08:00
erdenezul
9b022b187f
Merge pull request #1905 from bagerard/rework_in_test
minor rework in test
2018-10-10 16:29:53 +08:00
erdenezul
c3409d64dc
Merge pull request #1906 from vainu-arto/fix_listfield_validate
Fix invalid isinstance check in ListField.validate
2018-10-10 15:58:47 +08:00
Arto Jantunen
3c5c3b5026 Fix invalid isinstance check in ListField.validate
Using QuerySet directly would fail if QuerySetNoCache was used. Sadly the
test suite does not have a case where QuerySet would appear here, so adding
a test for this special case is not trivial.
2018-10-10 10:43:29 +03:00
erdenezul
f240f00d84
Merge pull request #1914 from bagerard/improve_code_quality
Improve code quality (mainly based on pylint findings)
2018-10-10 15:40:07 +08:00
Luca Di Gaspero
68c7764c63 Small fix to ensure to use the right document after pre_save_post_validation hook, which might modify the document itself (e.g., for adding a etag). 2018-10-08 17:06:08 +02:00
Bastien Gérard
adfb039ba6 Improve overall code quality (based on pylint findings) 2018-10-07 23:05:18 +02:00
Bastien Gérard
89416d9856 Fix deprecation warning about Hashable in py3.7 2018-10-05 16:17:55 +02:00
Bastien Gérard
9b6c972e0f renamed decorator needs_mongodb_x in test + fixes ref to mongo 2.4 from travis files 2018-10-02 22:03:55 +02:00
erdenezul
55fc04752a
Merge pull request #1903 from bagerard/mongodb_32
Fixed tests to allow support of MongoDB 3.2 + Drop Mongo2.4 from CI
2018-10-01 10:37:12 +08:00
Bastien Gérard
96f0919633 - Fixed tests to allow support of MongoDB 3.2
- Replaced MongoDB 2.4 tests in CI by MongoDB 3.2
2018-09-30 21:59:15 +02:00
erdenezul
17b140baf4
Merge pull request #1901 from bagerard/fix_side_effect_no_dereference_on_GenericReferenceField
Fix side effect of no_dereference on GenericReferenceField
2018-09-26 06:45:17 +08:00
Bastien Gérard
45c2151d0f Fix side effect of no_dereference on GenericReferenceField 2018-09-25 22:56:57 +02:00
erdenezul
1887f5b7e7
Merge pull request #1877 from bagerard/improve_ComplexField_validation_edge_case
Handles edge case when EmbeddedDocumentListField receives a Document and not a list
2018-09-18 14:49:24 +08:00
erdenezul
708d1c7a32
Merge pull request #1892 from bagerard/no_dereference_queryset_fix_side_effect
fix side effect from queryset.no_dereference
2018-09-18 14:40:14 +08:00
erdenezul
acf8c3015a
Merge pull request #1893 from bagerard/swat_url_fix
(CLONE) fix - allow url with underscore in domain
2018-09-18 14:29:55 +08:00
Bastien Gérard
f83ae5789b fix side effect from queryset's no_dereference #1677 2018-09-16 23:11:45 +02:00
Bastien Gérard
57ccfcfc1b Merge branch 'master' of https://github.com/MongoEngine/mongoengine into swat_url_fix 2018-09-16 23:03:16 +02:00
erdenezul
dd0fdcfdd4
Merge pull request #1889 from bagerard/compatible_pyton_2_3_improvement
improve 2-3 codebase compatibility
2018-09-15 00:44:38 +08:00
erdenezul
5c805be067
Merge pull request #1879 from last-partizan/fix-choices-display
fixed TypeError on translated choices
2018-09-11 18:34:00 +08:00
Sergey Tereschenko
e423380d7f Merge remote-tracking branch 'original-repo/master' into fix-choices-display 2018-09-11 13:24:52 +03:00
erdenezul
4d8bebc917
Merge pull request #1886 from bagerard/patch-4
Updating inheritance doc
2018-09-10 09:12:50 +08:00
Bastien Gérard
4314fa883f improve 2-3 codebase compatibility 2018-09-09 23:32:10 +02:00
Bastien Gérard
d6e39b362b Updating inheritance doc
Fixes #429
2018-09-09 14:52:08 +02:00
Bastien Gérard
f89214f9cf Fixes bug where an EmbeddedDocument that shares the same id of its parent Document could be missing updates when .save was called
Fixes #1768, Fixes #1685
2018-09-09 10:40:51 +02:00
erdenezul
d17cac8210
Merge pull request #1884 from erdenezul/limit_behavior
Clone of Limit behavior pr #1614
2018-09-07 15:48:54 +08:00
Erdenezul Batmunkh
aa49283fa9 fix trailing whitespace 2018-09-07 15:39:55 +08:00
erdenezul
e79ea7a2cf
Merge branch 'master' into limit_behaviour 2018-09-07 15:34:23 +08:00
erdenezul
8a1d280f19
Merge pull request #1881 from bagerard/refactoring_poor_assertions_in_tests
Refactored some poor assertions across test suite
2018-09-07 14:59:37 +08:00
erdenezul
6a8eb9562f
Merge pull request #1882 from bagerard/fix_doc_index_options
fix doc for meta index_options
2018-09-07 14:37:38 +08:00
erdenezul
8f76e1e344
Merge branch 'master' into refactoring_poor_assertions_in_tests 2018-09-07 14:37:28 +08:00
erdenezul
7b9f084e6b
Merge branch 'master' into fix_doc_index_options 2018-09-07 14:27:57 +08:00
erdenezul
5b1693a908
Merge pull request #1871 from bagerard/improve_query_counter
Fix few things related to query_counter context manager
2018-09-07 14:22:12 +08:00
erdenezul
fd7c00da49
Merge pull request #1883 from svanburen/patch-1
Update link to index options
2018-09-07 10:46:39 +08:00
Stefan VanBuren
7fc5ced3af
Update link to index options 2018-09-06 16:50:31 -04:00
Bastien Gérard
a86092fb64 fix doc for meta index_options 2018-09-06 22:33:33 +02:00
Bastien Gérard
003827e916 rewrote some poorly written assertions like: assertTrue(isinstance(a, b)) assertTrue(a==b) assertTrue(a!=b) assertTrue(a in b) 2018-09-06 21:47:06 +02:00
Sergey Tereschenko
b15673c525 fixed TypeError on translated choices
join expect strings, but if we use django ugettext_lazy like this:

    choices=[(1, _("One")), (2, _("Two"))]

there will be TypeError: sequence item 0: expected string, __proxy__ found

this commif fixes error by converting label to string
2018-09-05 11:53:15 +03:00
erdenezul
00363303b1
Merge pull request #1875 from bagerard/dax_py3
CLONE - return instead of raising StopIteration
2018-09-05 09:42:07 +08:00
erdenezul
48fbe890f8
Merge pull request #1876 from bagerard/get_rid_of_DictField_basecls
Remove DictField.basecls related code, it is useless
2018-09-05 09:38:52 +08:00
erdenezul
4179877cc7
Merge pull request #1878 from bagerard/fix_complex_datetime_default
Fix ComplexDateTime default value
2018-09-05 09:37:05 +08:00
Bastien Gérard
282b83ac08 Fix default value of ComplexDateTime + fixed descriptor .__get__ for class attribute 2018-09-04 23:48:07 +02:00
Bastien Gérard
193656e71b detect when EmbeddedDocumentListField receives an EmbeddedDocument instance instead of a list. It is a common mistake and the error wasn't meaningful (was fired from _from_son)
(relates to #1464)
2018-09-04 22:26:19 +02:00
Bastien Gérard
a25d127f36 Remove DictField.basecls related code, it is useless 2018-09-04 20:51:06 +02:00
Bastien Gérard
cf9df548ca reverted back to the StopIteration in queryset.next that one didnt have to be changed (test stalled) 2018-09-04 19:18:40 +02:00
Bastien Gérard
f29b93c762 Merge branch 'master' of https://github.com/MongoEngine/mongoengine into dax_py3 2018-09-04 16:06:45 +02:00
erdenezul
032ace40d1
Merge pull request #1668 from erdenezul/read_preference_parse
parse read_preference from conn_host #1665
2018-09-04 21:58:31 +08:00
erdenezul
f74dd1cb3c
Merge pull request #1591 from axu2/axu2-docs-patch-3
docs: use explicit register_delete_rule example
2018-09-04 21:50:29 +08:00
erdenezul
29889d1e35
Merge pull request #1868 from bagerard/yalon-master
CLONE - When building a query set from filters that reference the same field several times, do not assume each value is a dict
2018-09-04 21:17:49 +08:00
erdenezul
d6d19c4229
Merge pull request #1872 from bagerard/inc_operator_with_decimal
fix inc/dec operator with DecimalField + improve its doc
2018-09-04 21:07:04 +08:00
Bastien Gérard
ab08e67eaf fix inc/dec operator with decimal 2018-09-04 14:53:55 +02:00
erdenezul
00bf6ac258
Merge pull request #1874 from erdenezul/reduce-cycle-complexity
reduce cycle complexity using logic map
2018-09-04 20:52:26 +08:00
Erdenezul Batmunkh
b65478e7d9 trigger ci 2018-09-04 20:44:44 +08:00
Erdenezul Batmunkh
e83b529f1c flip value before changing op to inc 2018-09-04 20:38:42 +08:00
Erdenezul Batmunkh
408274152b reduce cycle complexity using logic map 2018-09-04 20:24:34 +08:00
Bastien Gérard
8ff82996fb Fix few things related to query_counter context manager:
- Improve doc
- Fix the fact that the context was modifying the initial profiling_level in case it was != 0
- Ignores 'killcursors' to fix flaky test that were impacted by killcursors queries (#1869)
2018-09-04 12:07:13 +02:00
Bastien Gérard
d59c4044b7 Merge branch 'master' of https://github.com/MongoEngine/mongoengine into yalon-master 2018-09-03 11:23:28 +02:00
erdenezul
3574e21e4f
Merge pull request #1859 from bagerard/fix_EmbeddedDocumentField_init_with_Document
Ensures EmbeddedDocumentField does not accepts references to Document
2018-09-03 17:22:08 +08:00
erdenezul
5a091956ef
Merge pull request #1862 from bagerard/document_name_param_for_indexes
Documented that it is possible to specify a 'name' for the indexes (using the dict definition)
2018-09-03 17:20:17 +08:00
erdenezul
14e9c58444
Merge pull request #1866 from bagerard/no_subclass_fixes
Fixes 2 bugs in no_subclasses context mgr
2018-09-03 17:19:39 +08:00
erdenezul
bfe5b03c69
Merge pull request #1867 from bagerard/pmatos_patch-1
Doc update: Clarify comment in validation example
2018-09-03 07:42:39 +08:00
Bastien Gérard
f96f7f840e Merge branch 'master' of https://github.com/MongoEngine/mongoengine into dax_py3 2018-09-02 15:36:35 +02:00
Bastien Gérard
a3bcf26dce Merge branch 'master' of https://github.com/MongoEngine/mongoengine into pmatos_patch-1 2018-09-02 15:27:37 +02:00
Bastien Gérard
a7852a89cc Fixes 2 bugs in no_subclasses context mgr (__exit__ swallows exception + repair feature) 2018-09-01 23:30:50 +02:00
erdenezul
1b0c761fc0
Merge pull request #1863 from bagerard/fix_string_format_bug
Fix string formatting bug in ReferenceField.validate
2018-09-01 19:52:37 +08:00
Bastien Gérard
5e4e8d4eda Merge branch 'master' of github.com:KCarretto/mongoengine 2018-09-01 12:04:23 +02:00
Bastien Gérard
bd524d2e1e Documented that it is possible to specify a name when using a dict to define an index 2018-09-01 00:12:49 +02:00
erdenezul
60fe919992
Merge pull request #1860 from bagerard/improve_doc_EmbeddedDocumentList.filter
Improve doc of EmbeddedDocumentList.filter,
2018-08-31 11:35:25 +08:00
erdenezul
b90063b170
Merge pull request #1861 from bagerard/minor_improvement_to_DateTimeField_default
minor improvement to DateTimeField doc
2018-08-31 11:32:40 +08:00
Bastien Gérard
d9fce49b08 minor improvement to DateTimeField doc 2018-08-30 22:46:37 +02:00
Bastien Gérard
5dbee2a270 Ensures EmbeddedDocumentField does not accepts references to Document classes in its constructor 2018-08-30 22:06:36 +02:00
Bastien Gérard
4779106139 Improve doc of EmbeddedDocumentList.filter and clarify that it does not supports operators like __lte, __gte, etc 2018-08-30 21:50:03 +02:00
erdenezul
bf2de81873
Merge pull request #1807 from bagerard/lazy_regex_compilation
Implemented lazy regex compiling for issue #1806
2018-08-30 21:33:10 +08:00
erdenezul
28cdedc9aa
Merge pull request #1857 from bagerard/fix_doc_of_delete_write_concern
Fix .delete doc of **write_concern
2018-08-30 21:28:59 +08:00
erdenezul
7e90571404
Merge pull request #1858 from bagerard/fix_index_creation_error_swallowed
Fix index creation error that was swallowed by hasattr under python2
2018-08-30 21:28:38 +08:00
erdenezul
42bbe63927
Merge pull request #1856 from bagerard/document_created_in_from_json_doc
Document the 'created' attribute of .from_json
2018-08-30 21:28:04 +08:00
Bastien Gérard
7ddbea697e fix CI that fails due to pypi + override BaseDict.get as it was missing 2018-08-30 14:33:57 +02:00
Bastien Gérard
b4860de34d Fix index creation error that was swallowed by hasattr under python2 (#1688) 2018-08-30 10:39:07 +02:00
Bastien Gérard
576f23d5fb Fix .delete doc of **write_concern as suggested by #1779 2018-08-28 22:58:13 +02:00
Bastien Gérard
86548fc7bf Document the attribute of .from_json 2018-08-28 22:42:51 +02:00
Terence D. Honles
b3b4d992fe add Q repr tests 2018-08-27 19:35:47 -07:00
Bastien Gérard
d72daf5f39 Fix BaseList bug with marked_as_changed getting called on error
+ Fix BaseDict as well since it suffers from the same problem
2018-08-27 22:34:11 +02:00
erdenezul
9ad959a478
Merge pull request #1845 from bagerard/clean_dead_code
remove dead code
2018-08-26 08:33:20 +08:00
erdenezul
cc00a321da
Merge pull request #1848 from bagerard/patch-4
Update gridfs.rst - make it clear that user must call Document.save()
2018-08-26 08:28:42 +08:00
erdenezul
de74273108
Merge pull request #1851 from bagerard/fix_IndexError_iter_while_modif_list
fix BaseList.__iter__ operator + minor improvements
2018-08-26 08:26:43 +08:00
Bastien Gérard
a7658c7573 fix BaseList.__iter__ operator (#1305) + minor improvements 2018-08-25 16:33:43 +02:00
Bastien Gérard
48a85ee6e0 update related tests 2018-08-20 00:10:27 +02:00
Bastien Gérard
461b789515 relates to (#710)
- Update gridfs.rst to make it clearer that you should save the Document hosting the GridFSProxy after calling .delete() or .replace() on the GridFSProxy
- updated GridFSProxy.__str__ so that it would always print both the filename and the grid_id. This should improve debugging
2018-08-19 23:45:08 +02:00
Bastien Gérard
b71ff6fbb8 fix validator of BinaryField. In fact bson.Binary fails if we give it unicode in input #273 2018-08-18 23:04:46 +02:00
Bastien Gérard
1bcdcce93a remove dead code 2018-08-18 00:03:17 +02:00
Terence D. Honles
c09bfca634 add __repr__ to Q and QCombination 2018-08-15 12:36:13 -07:00
erdenezul
36c5f02bfb
Merge pull request #1840 from bagerard/patch-2
Update signals doc - clarification on EmbeddedDocument
2018-08-15 20:41:04 +08:00
erdenezul
eae6e5d9a1
Merge pull request #1841 from bagerard/update_contributing_guidelines_python2_3
Contributing doc update - Clarify python 2 should be used to develop on mongoengine
2018-08-15 20:35:36 +08:00
Bastien Gérard
364813dd73 Clarify that you should use python 2 when developing on mongoengine #1837 2018-08-13 22:34:37 +02:00
Bastien Gérard
1a2b1f283b Update signals doc - clarification on EmbeddedDocument
Since there is a .save() method on EmbeddedDocument, you could be tempted to attach a pre_save event to an EmbeddedDocument (#1720). This update is an attempt to make this clearer.
2018-08-13 22:16:32 +02:00
erdenezul
a0e5cf4ecc
Merge pull request #1834 from JoschSan/patch-1
Update connecting.rst
2018-08-08 15:59:09 +08:00
JoschSan
820f7b4d93
Update connecting.rst
My suggest is append the parameter in authentication_source in authentication example, because was included in https://github.com/MongoEngine/mongoengine/pull/590/files
2018-08-07 14:29:15 -04:00
Bastien Gérard
727866f090 fix styling flake8 error from CI 2018-08-05 22:30:51 +02:00
Bastien Gérard
3d45cdc339 Implemented lazy regex compiling in Field classes to improve 'import mongoengine' performance 2018-08-03 23:36:26 +02:00
erdenezul
02a557aa67
Merge pull request #1825 from orsinium/date-field
Date field
2018-07-27 19:32:27 +08:00
Gram
6da27e5976 +changelog 2018-07-27 12:31:27 +03:00
Gram
19a6e324c4 +tests for date field 2018-07-20 17:37:23 +03:00
Gram
62eadbc174 +date field 2018-07-20 17:21:57 +03:00
Erdenezul Batmunkh
ae783d4f45 tweak tox.ini to pass tests 2018-07-16 17:25:29 +08:00
Erdenezul Batmunkh
1241a902e3 fix changelog 2018-07-16 09:49:16 +08:00
Erdenezul Batmunkh
fdba648afb fix pypi trigger 2018-07-16 09:19:50 +08:00
Emmanuel Leblond
b070e7de07
Fix travis's pypi release trigger 2018-06-29 15:47:28 +02:00
Tony Narlock
d0741946c7 Fix a typo (#1813)
an -> a
2018-06-28 17:19:48 +02:00
Tal Yalon
080226dd72 Fix issue #1286 and #844.: when building a query set from filters that reference the same field several times, do not assume each value is a dict 2018-06-22 14:16:17 +03:00
erdenezul
3cb6a5cfac
Merge pull request #1575 from zetaben/master
Make queryset aggregates obey read_preference
2018-05-24 22:23:49 +08:00
erdenezul
758971e068
Merge pull request #1799 from erdenezul/ensure_indexes_fails_on_slave
Ensure indexes fails on slave #1338
2018-05-23 16:09:51 +08:00
Erdenezul Batmunkh
8739ab9c66 fix syntax #1338 2018-05-23 15:59:18 +08:00
Erdenezul Batmunkh
e8e47c39d7 add changelog #1338 2018-05-23 15:54:44 +08:00
Erdenezul Batmunkh
446c101018 dont call ensure_indexes on slave #1338 2018-05-23 15:53:30 +08:00
erdenezul
3654591a1b
Merge pull request #1584 from etng/master
[fix]validation list field with multi choice values
2018-05-21 16:33:10 +08:00
erdenezul
7fb1c9dd35
Merge branch 'master' into master 2018-05-21 16:22:07 +08:00
erdenezul
0fffaccdf4
Merge pull request #1640 from erdenezul/fix_changed_field_reload
add fix for reload(fields) affect changed fields #1371
2018-05-21 16:16:37 +08:00
Erdenezul Batmunkh
5902b241f9 Merge branch 'fix_changed_field_reload' of github.com:erdenezul/mongoengine into fix_changed_field_reload 2018-05-21 16:07:31 +08:00
Erdenezul Batmunkh
784386fddc add changelog #1371 2018-05-21 16:07:08 +08:00
Erdenezul
d424583cbf fix flake8 error #1371 2018-05-21 16:06:04 +08:00
Erdenezul
290b821a3a add fix for reload(fields) affect changed fields #1371 2018-05-21 16:06:04 +08:00
erdenezul
a0dfa8d421
Merge pull request #1723 from chhsiao1981/dereference-add-check-dbref-type
defensive programming for v as an instance of DBRef in dereference.
2018-05-21 13:00:46 +08:00
erdenezul
ceb00f6748
Merge pull request #1796 from erdenezul/new_bulk_insert_test
modify bulk_insert test for pymongo3
2018-05-21 10:24:03 +08:00
Erdenezul Batmunkh
9bd328e147 query_counter fix 2018-05-21 10:04:59 +08:00
Erdenezul Batmunkh
6fb5c312c3 fix test error 2018-05-21 09:54:19 +08:00
Erdenezul Batmunkh
3f9ff7254f fix queryset tests 2018-05-21 09:46:23 +08:00
Erdenezul Batmunkh
f7a3acfaf4 query profiler test fix 2018-05-21 09:34:44 +08:00
Erdenezul Batmunkh
e4451ccaf8 insert_many uses only one insert 2018-05-21 09:24:40 +08:00
Erdenezul Batmunkh
2adb640821 modify bulk_insert test for pymongo3 2018-05-21 09:19:03 +08:00
erdenezul
765038274c
Merge pull request #1691 from rcscott/document-auto-create-index
Add documentation for auto_create_index to the Indexing section of the Docs
2018-05-21 08:55:25 +08:00
erdenezul
2cbdced974
Merge pull request #1793 from erdenezul/insert_pymongo3
use insert_one, insert_many and remove deprecated one #1491
2018-05-20 18:48:14 +08:00
Erdenezul Batmunkh
fc5d9ae100 pymongo3 does not support continue_on_error 2018-05-20 18:39:22 +08:00
Erdenezul Batmunkh
506168ab83 use write_concern class 2018-05-20 18:25:50 +08:00
Erdenezul Batmunkh
088fd6334b bulkwriteerror does not trigger 2018-05-20 18:06:59 +08:00
Erdenezul Batmunkh
94cda90a6e fix syntax 2018-05-20 17:56:19 +08:00
Erdenezul Batmunkh
78601d90c9 fix typo 2018-05-20 17:54:13 +08:00
Erdenezul Batmunkh
fa4ac95ecc catch bulkwriteerror 2018-05-20 17:49:49 +08:00
Erdenezul Batmunkh
dd4d4e23ad call insert method with set_write_concern 2018-05-20 15:51:02 +08:00
Erdenezul Batmunkh
acba86993d set_write_concern pymongo3 2018-05-20 15:43:19 +08:00
Erdenezul Batmunkh
0fc55451c2 fix style and ids need to be an array 2018-05-20 15:40:26 +08:00
Erdenezul Batmunkh
5c0bd8a810 fix inserted_ids 2018-05-20 15:40:26 +08:00
Erdenezul Batmunkh
1aebc95145 use insert_one, insert_many and remove deprecated one #1491 2018-05-20 15:40:26 +08:00
Erdenezul Batmunkh
1d3f20b666 fix style and ids need to be an array 2018-05-20 14:41:25 +08:00
Erdenezul Batmunkh
eb2e106871 Merge branch 'insert_pymongo3' of github.com:erdenezul/mongoengine into insert_pymongo3 2018-05-20 14:33:35 +08:00
Erdenezul Batmunkh
f9a887c8c6 fix inserted_ids 2018-05-20 14:33:12 +08:00
erdenezul
67ab810cb2
Merge branch 'master' into insert_pymongo3 2018-05-20 14:06:16 +08:00
Erdenezul Batmunkh
3e0d84383e use insert_one, insert_many and remove deprecated one #1491 2018-05-20 13:41:20 +08:00
erdenezul
d245ea3eaa
Merge pull request #1792 from erdenezul/update_pymongo3
use update_many, update_one #1491
2018-05-20 13:34:03 +08:00
Erdenezul Batmunkh
843fc03bf4 add changelog for update_one,update_many 2018-05-20 13:16:25 +08:00
Erdenezul Batmunkh
c83c635067 fix import order 2018-05-20 13:04:51 +08:00
Erdenezul Batmunkh
f605eb14e8 fix style 2018-05-20 12:54:24 +08:00
Erdenezul Batmunkh
fd02d77c59 drop pymongo 2.x support in update 2018-05-20 12:43:24 +08:00
Erdenezul Batmunkh
0da8fb379d Merge remote-tracking branch 'calgary/issue-1491' into update_pymongo3 2018-05-20 12:38:22 +08:00
Erdenezul Batmunkh
257a43298b use MongoClient.is_mongos in ensure indexes #1759 2018-05-20 12:31:27 +08:00
erdenezul
a2d3bcd571
Merge pull request #1562 from erdenezul/support_multiple_operator
support multiple operator #1510
2018-05-20 12:20:04 +08:00
erdenezul
d4142c2cdd
Merge pull request #1790 from erdenezul/compare_index_correct_behavior_text
Compare_indexes correct behavior for text index
2018-05-20 11:46:35 +08:00
Erdenezul Batmunkh
e50d66b303 skip mongodb 2.4 2018-05-20 11:26:30 +08:00
Erdenezul Batmunkh
08b6433843 fix compare_indexes for text indexes #1751 2018-05-20 11:03:13 +08:00
Erdenezul Batmunkh
8cd536aab5 Merge branch 'issue-1751' of github.com:th-erker/mongoengine into missing_text_index_ensure_indexes 2018-05-20 10:06:08 +08:00
erdenezul
2b495c648f
Merge pull request #1769 from mindojo-victor/master
Fix deprecation warning for StopIteration
2018-05-20 09:36:19 +08:00
erdenezul
06048b6d71
Merge pull request #1782 from nosahama/patch-1
Updated text-indexes.rst
2018-05-19 09:25:54 +08:00
erdenezul
bb22287336
Merge pull request #1784 from werat/fix_override_comment
Restore comment from cached value after queryset copy
2018-05-19 09:23:58 +08:00
erdenezul
a45942a966
Merge pull request #1789 from chidaobanjiu/patch-1
Fix typo
2018-05-19 09:20:47 +08:00
Benjamin Jiang
85d621846d
Fix typo 2018-05-16 11:37:32 +08:00
erdenezul
534acf8df2
Merge pull request #1731 from van4oza/patch-1
Insert null values fix
2018-05-15 14:18:24 +08:00
Andy Yankovsky
5a6d4387ea Restore comment from cached value after cursor copy 2018-05-07 23:17:12 +03:00
erdenezul
317e844886
Merge pull request #1765 from kushalmitruka/master
fixed : `pull` not working for EmbeddedDocumentListField, only working for ListFields #1534
2018-05-03 09:44:54 +08:00
Kushal Mitruka
b1f62a2735 added import in tests/queryset/transform.py for SON object 2018-05-01 21:08:43 +05:30
Kushal Mitruka
65e4fea4ef
added test cases for update pull queries 2018-05-01 20:32:38 +05:30
Emmanuel Nosa Evbuomwan
faca8512c5
Updated text-indexes.rst
The search statement under the `Ordering by text score` section uses `search` on the QuerySet object instead of `search_text` and thus raised an `AttributeError`

AttributeError: 'QuerySet' object has no attribute 'search'
2018-04-29 17:32:03 +01:00
erdenezul
2121387aa2
Merge pull request #1737 from CalgaryMichael/remove-pushall
Removing the usage of the '$pushAll' operator
2018-04-17 17:50:52 +08:00
erdenezul
72c4444a60
Merge branch 'master' into remove-pushall 2018-04-17 16:38:08 +08:00
erdenezul
2d8d2e7e6f
Merge pull request #1771 from bchrobot/add-lazy-reference-documentation
Add documentation for LazyReference and GenericLazyReference fields.
2018-04-12 23:12:12 +08:00
Benjamin Chrobot
49bff5d544
Add documentation for LazyReference and GenericLazyReference fields. 2018-04-12 10:47:52 -04:00
Victor
806a80cef1 Fixes #1641
There's no need to explicitly raise StopIteration as that's what a bare return statement does for a generator function - so yes they're the same.
As of late 2014 return is correct and raise StopIteration for ending a generator is on a depreciation schedule. See PEP 479 for full details.
https://stackoverflow.com/q/14183803/248296
https://www.python.org/dev/peps/pep-0479/
2018-04-09 17:29:09 +03:00
Kushal Mitruka
c6f0d5e478 fixed pull queries for embeddeddocumentlistfields
Updated mongoengine.queryset.transform.update method to handle EmbeddedDocuementListField during pull operations in DB using mongoegning ORM
fixed : .udpate(pull__emb_doc__emb_doc_list=doc)
2018-04-01 20:11:22 +05:30
erdenezul
bf30aba005
Merge pull request #1763 from malthejorgensen/patch-2
Docs, queryset.update(): Fix backtick mistake
2018-03-31 18:02:00 +08:00
Malthe Jørgensen
727778b730
Docs, queryset.update(): Fix backtick mistake
Code should be marked with double backticks
2018-03-30 20:41:39 +02:00
erdenezul
b081ffce50
Merge pull request #1762 from malthejorgensen/patch-1
queryset.update: `full_result`-argument not clearly described
2018-03-30 08:31:35 +08:00
Malthe Jørgensen
e46779f87b
Docs, queryset.update: full_result-arg not clearly described
The documentation for the `full_result`-argument to `queryset.update()`
can be read as returning the update documents/objects, whereas it's
really returning just the full "PyMongo result dictionary".

This commit adds some wording and an example dictionary,
to make it clear what the behavior is.
2018-03-27 14:14:08 +02:00
Stefan Wojcik
dabe8c1bb7 highlight places where ValidationError is raised outside of validate() method 2018-03-14 14:26:46 -04:00
erdenezul
4042f88bd8
Merge pull request #1753 from JohnAD/master
Edit EmbeddedDocumentListField update() doc
2018-03-11 19:35:13 +08:00
John Dupuy
a0947d0c54 Edit EmbeddedDocumentListField update() doc 2018-03-10 23:24:04 -06:00
Thomas Erker
a34fd9ac89 Add testcase for #1751 2018-03-08 11:25:36 +00:00
estein-de
aa68322641 MongoDB wants dates stored in UTC, but the functions used in this documentation to generate datetime objects would use server's local timezone - fix it! (#1662) 2018-02-27 09:43:09 -05:00
KCarretto
2d76aebb8e
Fixed formatting issue 2018-02-21 05:02:35 -05:00
erdenezul
7cc1d23bc7
Merge pull request #1655 from werat/fix_update_pull_in
Fix pull+in update queries
2018-02-21 13:36:04 +08:00
Andy Yankovsky
0bd2103a8c Add test for document update 2018-02-20 00:02:12 +03:00
Sangmin In
7d8916b6e9 fix case inconsistent casing of "MongoDB" (#1744) 2018-02-10 10:59:47 -05:00
swathi
8b5df3ca17 fix - allow url with underscore in domain 2018-02-08 12:31:40 -08:00
erdenezul
ffdfe99d37
Merge pull request #1636 from erdenezul/fix_shard_key_modify
Fix Document.modify fail on sharded collection #1569
2018-02-06 07:25:42 +08:00
Ivan Pogrebkov
7efa67e7e6
reverse to 'style fix' 2018-02-05 12:35:06 +03:00
Ivan Pogrebkov
d69808c204
oh, ok... 2018-02-05 12:33:58 +03:00
Ivan Pogrebkov
de360c61dd
removed useless lines 2018-02-05 04:26:25 +03:00
Ivan Pogrebkov
6b04ddfad1
>< 2018-02-05 04:24:03 +03:00
Ivan Pogrebkov
0d854ce906
style fix 2018-02-05 03:24:53 +03:00
Calgary Michael
38fdf26405 added tests for push and push_all 2018-02-04 10:23:50 -06:00
Calgary Michael
6835c15d9b fixing bug in previous commit 2018-02-02 22:41:07 -06:00
Calgary Michael
fa38bfd4e8 made set_write_concern python2.7 compatible 2018-02-02 22:30:06 -06:00
Calgary Michael
4d5c6d11ab removed deprecated warning for 'update' method 2018-02-02 22:04:30 -06:00
Calgary Michael
9e80da705a removed usage of 'pushAll' operator 2018-02-02 21:47:04 -06:00
erdenezul
9b04391f82
Merge pull request #1736 from vainu-arto/register-connection-match-connection
Add db parameter to register_connection #606
2018-02-02 16:46:45 +08:00
Arto Jantunen
8f6c0796e3 Add db parameter to register_connection
This is done to make it compatible with the connect function.
2018-02-02 10:26:36 +02:00
erdenezul
326fcf4398
Merge pull request #1735 from esmail/patch-1
Update `post_save` signal documentation to reflect #594
2018-02-02 15:06:17 +08:00
Esmail
fdda27abd1
Update post_save signal documentation to reflect #594 2018-02-01 12:58:10 -05:00
Ivan Pogrebkov
7e8c62104a
null=True now usefull 2018-01-26 11:15:12 +03:00
Ivan Pogrebkov
fb213f6e74
Update document.py 2018-01-26 11:12:02 +03:00
Ivan Pogrebkov
22e75c1691
Insert null values fix
https://stackoverflow.com/questions/42601950/how-to-store-a-null-value-in-mongodb-via-mongoengine
2018-01-26 10:55:44 +03:00
Chuan-Heng Hsiao
919f221be9 defensive programming for v as an instance of DBRef when accessing v.collection in dereference 2018-01-11 07:58:24 -05:00
erdenezul
da7d64667e
Merge pull request #1571 from erdenezul/reverse_delete_rule_with_pull
add test case for reverse_delete_rule with pull #1519
2017-12-23 11:32:09 +08:00
erdenezul
d19c6a1573
Merge pull request #1714 from erdenezul/1712-cached-reference-push-fix
update fields argument when given #1172
2017-12-23 00:18:19 +08:00
Erdenezul Batmunkh
5cd23039a0 Merge branch '1712-cached-reference-push-fix' of github.com:erdenezul/mongoengine into 1712-cached-reference-push-fix 2017-12-23 00:02:43 +08:00
Erdenezul
19b18d3d0a add changelog #1712 2017-12-23 00:02:03 +08:00
Erdenezul
101947da8b update fields argument when given #1172 2017-12-23 00:00:57 +08:00
Emmanuel Leblond
d3c3c23630
Merge pull request #1717 from touilleMan/fix-pymongo-mongodb2.4
Fix travis tests with mongodb 2.4 & pymongo 3
2017-12-22 16:19:14 +01:00
Erdenezul Batmunkh
abc14316ea Merge branch '1712-cached-reference-push-fix' of github.com:erdenezul/mongoengine into 1712-cached-reference-push-fix 2017-12-22 21:09:07 +08:00
Erdenezul
b66621f9c6 add changelog #1712 2017-12-22 21:08:35 +08:00
Erdenezul
aa5510531d update fields argument when given #1172 2017-12-22 21:07:35 +08:00
Emmanuel Leblond
12b846586c
Fix travis tests with mongodb 2.4 & pymongo 3 2017-12-22 13:23:03 +01:00
erdenezul
b705f5b743
Merge pull request #1716 from MongoEngine/revert-1645-inc_cov
Revert "add tests to increase code coverage"
2017-12-22 20:22:37 +08:00
erdenezul
18a5fba42b
Revert "add tests to increase code coverage" 2017-12-22 20:19:21 +08:00
erdenezul
b5a3b6f86a
Merge pull request #1645 from erdenezul/inc_cov
add tests to increase code coverage
2017-12-22 20:14:31 +08:00
erdenezul
00f2eda576
Merge branch 'master' into 1712-cached-reference-push-fix 2017-12-22 19:45:51 +08:00
erdenezul
c70d252dc3
Merge branch 'master' into 1712-cached-reference-push-fix 2017-12-22 19:44:53 +08:00
Emmanuel Leblond
2f088ce29e
Merge pull request #1710 from esmail/patch-1
One-character typo fix ("that" -> "than")
2017-12-22 12:27:47 +01:00
Emmanuel Leblond
ff408c604b
Merge pull request #1703 from erdenezul/invalid_instance_genericembedded
fix validation error for invalid embedded document instance #1067
2017-12-22 12:23:31 +01:00
Erdenezul
6621c318db add changelog #1712 2017-12-20 15:28:33 +08:00
Erdenezul
22a8ad2fde update fields argument when given #1172 2017-12-20 15:17:54 +08:00
Esmail
7674dc9b34
One-character typo fix ("that" -> "than") 2017-12-05 15:14:15 -05:00
Erdenezul
9e0ca51c2f remove merge conflict after rebase #1067 2017-12-01 08:40:51 +08:00
Erdenezul
961629d156 add changelog into dev #1067 2017-12-01 08:40:00 +08:00
Erdenezul Batmunkh
2cbebf9c99 move changes to development 2017-12-01 08:38:41 +08:00
Erdenezul Batmunkh
08a4deca17 change description in changelog 2017-12-01 08:37:53 +08:00
Erdenezul Batmunkh
ce9ea7baad add changelog 2017-12-01 08:37:53 +08:00
Erdenezul Batmunkh
b35efb9f72 fix validatione error for invalid embedded document instance #1067 2017-12-01 08:37:53 +08:00
Emmanuel Leblond
c45dfacb41
Update changelog 2017-11-29 16:41:42 +01:00
Emmanuel Leblond
91152a7977
Merge pull request #1704 from touilleMan/lazyref-improvements
Improve LazyReferenceField and GenericLazyReferenceField with nested …
2017-11-29 16:38:11 +01:00
Erdenezul Batmunkh
0ce081323f move changes to development 2017-11-22 22:19:50 +08:00
Erdenezul Batmunkh
79486e3393 change description in changelog 2017-11-22 19:27:35 +08:00
Erdenezul Batmunkh
60758dd76b add changelog 2017-11-22 18:56:12 +08:00
Emmanuel Leblond
e74f659015
Improve LazyReferenceField and GenericLazyReferenceField with nested fields 2017-11-22 11:44:49 +01:00
Erdenezul Batmunkh
c1c09fa6b4 fix validatione error for invalid embedded document instance #1067 2017-11-21 21:56:26 +08:00
Emmanuel Leblond
47c7cb9327
Ignore style I202 rule (see https://github.com/PyCQA/flake8-import-order/issues/122) 2017-11-15 20:44:36 +01:00
Emmanuel Leblond
4d6256e1a1
Merge pull request #1675 from erdenezul/push_list_of_list
use each modifier only with $position  #1673
2017-11-15 08:43:29 +01:00
Emmanuel Leblond
13180d92e3
Merge pull request #1652 from erdenezul/generic_embedded_query
Subfield resolve error in generic_emdedded_document query #1651
2017-11-13 15:29:26 +01:00
kcarretto
6b38ef3c9f Fixed format string issue in mongoengine error message. 2017-11-11 03:36:28 -05:00
Ryan Scott
4f5b0634ad Add documentation for auto_create_index to the Indexing section of the Docs 2017-11-07 16:26:01 -05:00
Emmanuel Leblond
ea25972257
Bump version to 0.15.0 2017-11-06 14:40:59 +01:00
Emmanuel Leblond
b6168898ec
Merge pull request #1690 from touilleMan/lazyreference-field
Add LazyReferenceField & GenericLazyReferencField
2017-11-06 14:37:43 +01:00
Emmanuel Leblond
da33cb54fe
Correct style 2017-11-06 14:11:11 +01:00
Emmanuel Leblond
35d0458228
Add GenericLazyReferenceField 2017-11-06 12:29:19 +01:00
Emmanuel Leblond
e6c0280b40
Add LazyReferenceField 2017-10-31 18:58:42 +01:00
Mandeep Singh
15451ff42b return instead of raising StopIteration 2017-10-17 00:11:47 +05:30
Erdenezul
9ab856e186 use each modifier only with #1673 2017-10-10 10:34:34 +08:00
Erdenezul
6e2db1ced6 read_preference from parse_uri only PYMONGO_3 #1665 2017-10-03 09:23:17 +08:00
Erdenezul Batmunkh
5c4ce8754e run tests only pymongo3 #1565 2017-10-02 23:15:37 +08:00
Erdenezul Batmunkh
416486c370 use read_preference only pymongo3.x #1665 2017-10-02 23:13:25 +08:00
Erdenezul Batmunkh
2f075be6f8 parse read_preference from conn_host #1665 2017-10-02 22:46:27 +08:00
Stefan Wojcik
a1494c4c93 v0.14.3 version bump 2017-10-01 17:31:10 -04:00
Stefan Wojcik
d79ab5ffeb remove {nospam} from author_email & maintainer_email (PyPI doesnt validate those anymore) 2017-10-01 17:05:28 -04:00
Stefan Wojcik
01526a7b37 v0.14.1 version bump + updated changelog 2017-10-01 16:32:02 -04:00
Stefan Wojcik
091a02f737 minor .travis.yml comment correction [ci skip] 2017-10-01 16:09:10 -04:00
Erdenezul
aa4996ef28 fix bug query subfield in generic_embedded_document #1651 2017-09-15 11:18:24 +08:00
Andy Yankovsky
2f4e2bde6b Update AUTHORS 2017-09-14 21:02:53 +03:00
Andy Yankovsky
e90f6a2fa3 Fix update via pull__something__in=[] 2017-09-14 20:28:15 +03:00
Erdenezul Batmunkh
be8f1b9fdd add failing test for generic_emdedded_document query #1651 2017-09-14 22:24:27 +09:00
Erdenezul Batmunkh
ba99190f53 add tests to increase coverage 2017-09-10 13:09:20 +09:00
Erdenezul Batmunkh
70088704e2 add tests to increase code coverage 2017-09-10 01:37:17 +09:00
Erdenezul
02733e6e58 fix flake8 error #1371 2017-09-02 12:00:57 +09:00
Erdenezul
44732a5dd9 add fix for reload(fields) affect changed fields #1371 2017-09-02 02:05:27 +09:00
Emmanuel Leblond
5bdd35464b Merge pull request #1632 from srinivasreddy/inequality
Add missing dunder method  - __ne__ to the class GridFSProxy class
2017-08-31 10:15:44 +02:00
Erdenezul
1eae97731f Fix Document.modify fail on sharded collection #1569 2017-08-30 12:04:04 +08:00
Emmanuel Leblond
0325a62f18 Merge pull request #1626 from erdenezul/pointfield_aspymongo
added as_pymongo test for PointField #1311
2017-08-28 13:36:35 +02:00
Emmanuel Leblond
3a5538813c Merge pull request #1616 from srinivasreddy/clean_up
Add six.moves.range instead of xrange
2017-08-28 13:34:17 +02:00
Emmanuel Leblond
1f1b4b95ce Merge pull request #1617 from srinivasreddy/python36
Add python 3.6 support
2017-08-28 13:33:56 +02:00
Emmanuel Leblond
8c3ed57ecc Merge pull request #1630 from MongoEngine/improve-perfs
Improve perfs by removing SemiStrictDict
2017-08-28 11:23:35 +02:00
Srinivas Reddy Thatiparthy
dc8a64fa7d Add missing dunder method - __ne__ to the class GridFSProxy class 2017-08-25 22:02:47 +05:30
Erdenezul
0d1e72a764 Merge branch 'pointfield_aspymongo' of github.com:erdenezul/mongoengine into pointfield_aspymongo 2017-08-25 11:15:51 +08:00
Erdenezul
9b3fe09508 added as_pymongo test for PointField #1311 2017-08-25 11:15:03 +08:00
Srinivas Reddy Thatiparthy
7c0cfb1da2 Add six.moves.range instead of xrange 2017-08-25 00:04:04 +05:30
Srinivas Reddy Thatiparthy
66429ce331 Add python 3.6 support 2017-08-25 00:02:39 +05:30
Emmanuel Leblond
bce859569f
Remove SemiStrictDict to improve perfs 2017-08-24 20:01:09 +02:00
Emmanuel Leblond
425fb8905b Merge pull request #1631 from MongoEngine/fix_travis_ci
Fix .install_mongodb_on_travis.sh script (all credit to @erdenezul)
2017-08-24 19:48:15 +02:00
Emmanuel Leblond
4f59c7f77f
Expose to user mongoengine.base.NON_FIELD_ERRORS
This variable is used to set the field containing the errors raised in a
clean function. Given those function are user-defined, users should be
able to get the name of the field to easily retreive their custom errors.
2017-08-24 19:47:40 +02:00
Emmanuel Leblond
21d1faa793
Fix .install_mongodb_on_travis.sh script (all credit to @erdenezul) 2017-08-24 19:34:22 +02:00
Erdenezul
b9f3991d03 added as_pymongo test for PointField #1311 2017-08-18 09:31:26 +08:00
Paulo Matos
c4de879b20 Clarify comment in validation example 2017-08-11 09:09:33 +02:00
Ali Turki
ee5686e91a test case for #1602 2017-08-07 11:12:15 +01:00
Ali
2a795e9138 QuerySet limit function now returns all docs in cursor when 0 is passed 2017-08-04 11:31:29 +01:00
erdenezul
9a6aa8f8c6 Merge branch 'master' into support_multiple_operator 2017-07-31 22:53:01 +08:00
Stefan Wójcik
3794b181d5 Support for $position with the $push operator (#1566) 2017-07-31 15:36:35 +02:00
Erdenezul Batmunkh
f09256a24e Fix modify tests #1565 2017-07-31 18:49:52 +08:00
Erdenezul Batmunkh
34fca9d6f5 Add clear comment and tests for positional push #1565 2017-07-31 18:32:34 +08:00
Erdenezul Batmunkh
433f10ef93 position support singular value #1565 2017-07-31 05:15:34 +00:00
Bo.Yi
9f02f71c52 [fix]fix some personal hobby 2017-07-16 18:47:20 +08:00
Erdenezul Batmunkh
3dcc9bc143 use explicit tests and fix unneccessary indent #1565 2017-07-13 22:59:21 +08:00
erdenezul
7311895894 Merge branch 'master' into support_position_in_push 2017-07-13 20:08:57 +08:00
Davidrjx
a7cab51369 Use a set literal in _clean_settings (#1585) 2017-07-13 12:07:36 +02:00
Alex Xu
437b11af9a docs: use explicit register_delete_rule example
The previous example of creating bi-directional delete rules was vague since the example defined only one class and the relationship between "Foo" and "Bar" wasn't clear. I added a more explicit example where the relationship between the two classes is explicit.
2017-07-10 16:43:24 -04:00
Bo.Yi
820b5cbb86 [fix]pass test case and fix field type error 2017-07-06 16:07:51 +08:00
Bo.Yi
e6a30f899c [fix]validation list field with multi choice values 2017-07-06 14:57:03 +08:00
Benoit Larroque
0bc6507df3 Make queryset aggregates obey read_preference 2017-06-27 08:49:06 +02:00
Erdenezul Batmunkh
71c3c632d7 add test case for reverse_delete_rule with pull #1519 2017-06-19 06:01:28 +00:00
Erdenezul Batmunkh
99a5f2cd9d EmbeddedDocument should not have save method #1552 2017-06-19 05:06:07 +00:00
Erdenezul Batmunkh
fb00b79d19 add docs for positional push operator #1565 2017-06-19 03:28:34 +00:00
Erdenezul Batmunkh
7782aa7379 do not test position push in mongodb_v2.4 #1565 2017-06-19 03:11:59 +00:00
Erdenezul Batmunkh
f3ee4a5dac add tests for push operator #1565 2017-06-19 02:59:17 +00:00
Stefan Wojcik
a8d6e59a7a minor tweaks to code quality in _fields_to_dbfields 2017-06-18 17:25:39 -07:00
Danil
1d4b1870cf to_db_fields fix (#1553) 2017-06-18 17:04:46 -07:00
Erdenezul Batmunkh
f63ad2dd69 dont test in mongoDB v2.4 #1565 2017-06-15 07:36:14 +00:00
Erdenezul Batmunkh
6903eed4e7 support position in 'push' #1565 2017-06-15 06:08:40 +00:00
Erdenezul Batmunkh
b9e922c658 support multiple operator #1510 2017-06-12 04:50:13 +00:00
Freyr
54d8c64ad5 Fixes query operations on BinaryFields
Overrides prepare_query_value in BinaryField to wrap the query value in
bson.binary.Binary. This was already done in the to_mongo call when
saving a document, but query operations required the user to convert the
value manually.

Fixes https://github.com/MongoEngine/mongoengine/issues/1127
2017-05-25 10:46:16 -04:00
José Enrique Carrillo Pino
2f1fe5468e Fix empty string casted to datetime today in DateTimeField (#1533) 2017-05-12 12:59:14 -04:00
lanf0n
24d15d4274 fix typo in the save() method's docstring (#1551) 2017-05-11 10:06:36 -04:00
Stefan Wojcik
0bc7aa52d8 more docs tweaks [ci skip] 2017-05-08 00:14:42 -04:00
Stefan Wojcik
e52603b4a7 ver bump to v0.14.0 + changelog/upgrade docs update 2017-05-08 00:12:26 -04:00
Stefan Wójcik
3b88712402 Cleaner as_pymongo (#1549) 2017-05-08 00:02:42 -04:00
Stefan Wojcik
33e9ef2106 dont test pypy3 temporarily 2017-05-07 21:37:38 -04:00
Stefan Wojcik
689fe4ed9a Revert "use a newer pypy3 (https://github.com/travis-ci/travis-ci/issues/6277)"
This reverts commit 944d1c0a4ab817a9f0dcd124bbf8456520367e8b.
2017-05-07 21:37:14 -04:00
Stefan Wojcik
b82d026f39 Revert "fix tox.ini"
This reverts commit c00914bea2951d53a9b8021e3facf406e8d9109e.
2017-05-07 21:37:05 -04:00
Stefan Wojcik
009059def4 revert #1497 2017-05-07 21:29:13 -04:00
Stefan Wójcik
03ff61d113 better db_field validation (#1547) 2017-05-07 21:11:14 -04:00
Stefan Wojcik
c00914bea2 fix tox.ini 2017-05-07 20:32:52 -04:00
143 changed files with 30381 additions and 22503 deletions

143
.github/workflows/github-actions.yml vendored Normal file
View File

@ -0,0 +1,143 @@
name: MongoengineCI
on:
# All PR
pull_request:
# master branch merge
push:
branches:
- master
# release tags
create:
tags:
- 'v[0-9]+\.[0-9]+\.[0-9]+*'
env:
MONGODB_3_6: 3.6.14
MONGODB_4_0: 4.0.23
MONGODB_4_2: 4.2
MONGODB_4_4: 4.4
PYMONGO_3_4: 3.4
PYMONGO_3_6: 3.6
PYMONGO_3_9: 3.9
PYMONGO_3_11: 3.11
MAIN_PYTHON_VERSION: 3.7
jobs:
linting:
# Run pre-commit (https://pre-commit.com/)
# which runs pre-configured linter & autoformatter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- run: bash .github/workflows/install_ci_python_dep.sh
- run: pre-commit run -a
test:
# Test suite run against recent python versions
# and against a few combination of MongoDB and pymongo
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3]
MONGODB: [$MONGODB_4_0]
PYMONGO: [$PYMONGO_3_11]
include:
- python-version: 3.7
MONGODB: $MONGODB_3_6
PYMONGO: $PYMONGO_3_9
- python-version: 3.7
MONGODB: $MONGODB_4_2
PYMONGO: $PYMONGO_3_6
- python-version: 3.7
MONGODB: $MONGODB_4_4
PYMONGO: $PYMONGO_3_11
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: install mongo and ci dependencies
run: |
bash .github/workflows/install_mongo.sh ${{ matrix.MONGODB }}
bash .github/workflows/install_ci_python_dep.sh
bash .github/workflows/start_mongo.sh ${{ matrix.MONGODB }}
- name: tox dry-run (to pre-install venv)
run: tox -e $(echo py${{ matrix.python-version }}-mg${{ matrix.PYMONGO }} | tr -d . | sed -e 's/pypypy/pypy/') -- -a "-k=test_ci_placeholder"
- name: Run test suite
run: tox -e $(echo py${{ matrix.python-version }}-mg${{ matrix.PYMONGO }} | tr -d . | sed -e 's/pypypy/pypy/') -- -a "--cov=mongoengine"
- name: Send coverage to Coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github
if: ${{ matrix.python-version == env.MAIN_PYTHON_VERSION }}
run: coveralls
build_doc_dryrun:
# ensures that readthedocs can be built continuously
# to avoid that it breaks when new releases are being created
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: install python dep
run: |
pip install -e .
pip install -r docs/requirements.txt
- name: build doc
run: |
cd docs
make html-readthedocs
build-n-publish-dummy:
runs-on: ubuntu-latest
needs: [linting, test, build_doc_dryrun]
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: build dummy wheel for test-pypi
run: |
pip install wheel
python setup.py egg_info -b ".dev`date '+%Y%m%d%H%M%S'`" build sdist bdist_wheel
# - name: publish test-pypi
# # Although working and recommended, test-pypi has a limit
# # in the size of projects so it's better to avoid publishing
# # until there is a way to garbage collect these dummy releases
# uses: pypa/gh-action-pypi-publish@master
# with:
# password: ${{ secrets.test_pypi_token }}
# repository_url: https://test.pypi.org/legacy/
build-n-publish:
runs-on: ubuntu-latest
needs: [linting, test, build_doc_dryrun, build-n-publish-dummy]
if: github.event_name == 'create' && startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
# todo separate build from publish
# https://stackoverflow.com/questions/59349905/which-properties-does-github-event-in-a-github-workflow-have
- name: build dummy wheel for test-pypi
run: |
pip install wheel
python setup.py sdist bdist_wheel
- name: publish pypi
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.pypi_token }}

View File

@ -0,0 +1,5 @@
#!/bin/bash
pip install --upgrade pip
pip install coveralls
pip install pre-commit
pip install tox

18
.github/workflows/install_mongo.sh vendored Normal file
View File

@ -0,0 +1,18 @@
#!/bin/bash
MONGODB=$1
# Mongo > 4.0 follows different name convention for download links
mongo_build=mongodb-linux-x86_64-${MONGODB}
if [[ "$MONGODB" == *"4.2"* ]]; then
mongo_build=mongodb-linux-x86_64-ubuntu1804-v${MONGODB}-latest
elif [[ "$MONGODB" == *"4.4"* ]]; then
mongo_build=mongodb-linux-x86_64-ubuntu1804-v${MONGODB}-latest
fi
wget http://fastdl.mongodb.org/linux/$mongo_build.tgz
tar xzf $mongo_build.tgz
mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
$mongodb_dir/bin/mongod --version

9
.github/workflows/start_mongo.sh vendored Normal file
View File

@ -0,0 +1,9 @@
#!/bin/bash
MONGODB=$1
mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
mkdir $mongodb_dir/data
$mongodb_dir/bin/mongod --dbpath $mongodb_dir/data --logpath $mongodb_dir/mongodb.log --fork
mongo --eval 'db.version();' # Make sure mongo is awake

11
.gitignore vendored
View File

@ -1,8 +1,15 @@
.*
!.gitignore
*~
*.py[co]
.*.sw[po]
.cache/
.coverage
.coveragerc
.env
.idea/
.pytest_cache/
.tox/
.eggs/
*.egg
docs/.build
docs/_build
@ -13,8 +20,6 @@ env/
.settings
.project
.pydevproject
tests/test_bugfix.py
htmlcov/
venv
venv3
scratchpad

View File

@ -1,23 +0,0 @@
#!/bin/bash
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
if [ "$MONGODB" = "2.4" ]; then
echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-10gen=2.4.14
sudo service mongodb start
elif [ "$MONGODB" = "2.6" ]; then
echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-org-server=2.6.12
# service should be started automatically
elif [ "$MONGODB" = "3.0" ]; then
echo "deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-org-server=3.0.14
# service should be started automatically
else
echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0."
exit 1
fi;

View File

@ -1,22 +0,0 @@
pylint:
disable:
# We use this a lot (e.g. via document._meta)
- protected-access
options:
additional-builtins:
# add xrange and long as valid built-ins. In Python 3, xrange is
# translated into range and long is translated into int via 2to3 (see
# "use_2to3" in setup.py). This should be removed when we drop Python
# 2 support (which probably won't happen any time soon).
- xrange
- long
pyflakes:
disable:
# undefined variables are already covered by pylint (and exclude
# xrange & long)
- F821
ignore-paths:
- benchmark.py

26
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,26 @@
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-merge-conflict
- id: debug-statements
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/ambv/black
rev: 21.5b2
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v2.19.1
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/pycqa/isort
rev: 5.8.0
hooks:
- id: isort

20
.readthedocs.yml Normal file
View File

@ -0,0 +1,20 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: docs/requirements.txt
# docs/conf.py is importing mongoengine
# so mongoengine needs to be installed as well
- method: setuptools
path: .

View File

@ -1,102 +0,0 @@
# For full coverage, we'd have to test all supported Python, MongoDB, and
# PyMongo combinations. However, that would result in an overly long build
# with a very large number of jobs, hence we only test a subset of all the
# combinations:
# * MongoDB v2.4 & v3.0 are only tested against Python v2.7 & v3.5.
# * MongoDB v2.4 is tested against PyMongo v2.7 & v3.x.
# * MongoDB v3.0 is tested against PyMongo v3.x.
# * MongoDB v2.6 is currently the "main" version tested against Python v2.7,
# v3.5, PyPy & PyPy3, and PyMongo v2.7, v2.8 & v3.x.
#
# Reminder: Update README.rst if you change MongoDB versions we test.
language: python
python:
- 2.7
- 3.5
- pypy
- pypy3.3-5.2-alpha1
env:
- MONGODB=2.6 PYMONGO=2.7
- MONGODB=2.6 PYMONGO=2.8
- MONGODB=2.6 PYMONGO=3.0
matrix:
# Finish the build as soon as one job fails
fast_finish: true
include:
- python: 2.7
env: MONGODB=2.4 PYMONGO=2.7
- python: 2.7
env: MONGODB=2.4 PYMONGO=3.0
- python: 2.7
env: MONGODB=3.0 PYMONGO=3.0
- python: 3.5
env: MONGODB=2.4 PYMONGO=2.7
- python: 3.5
env: MONGODB=2.4 PYMONGO=3.0
- python: 3.5
env: MONGODB=3.0 PYMONGO=3.0
before_install:
- bash .install_mongodb_on_travis.sh
install:
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev
python-tk
- travis_retry pip install --upgrade pip
- travis_retry pip install coveralls
- travis_retry pip install flake8 flake8-import-order
- travis_retry pip install tox>=1.9
- travis_retry pip install "virtualenv<14.0.0" # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32)
- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test
# Cache dependencies installed via pip
cache: pip
# Run flake8 for py27
before_script:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then flake8 .; else echo "flake8 only runs on py27"; fi
script:
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
# For now only submit coveralls for Python v2.7. Python v3.x currently shows
# 0% coverage. That's caused by 'use_2to3', which builds the py3-compatible
# code in a separate dir and runs tests on that.
after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi
notifications:
irc: irc.freenode.org#mongoengine
# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
branches:
only:
- master
- /^v.*$/
# Whenever a new release is created via GitHub, publish it on PyPI.
deploy:
provider: pypi
user: the_drow
password:
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
# create a source distribution and a pure python wheel for faster installs
distributions: "sdist bdist_wheel"
# only deploy on tagged commits (aka GitHub releases) and only for the
# parent repo's builds running Python 2.7 along with dev PyMongo (we run
# Travis against many different Python and PyMongo versions and we don't
# want the deploy to occur multiple times).
on:
tags: true
repo: MongoEngine/mongoengine
condition: "$PYMONGO = 3.0"
python: 2.7

20
AUTHORS
View File

@ -243,3 +243,23 @@ that much better:
* Victor Varvaryuk
* Stanislav Kaledin (https://github.com/sallyruthstruik)
* Dmitry Yantsen (https://github.com/mrTable)
* Renjianxin (https://github.com/Davidrjx)
* Erdenezul Batmunkh (https://github.com/erdenezul)
* Andy Yankovsky (https://github.com/werat)
* Bastien Gérard (https://github.com/bagerard)
* Trevor Hall (https://github.com/tjhall13)
* Gleb Voropaev (https://github.com/buggyspace)
* Paulo Amaral (https://github.com/pauloAmaral)
* Gaurav Dadhania (https://github.com/GVRV)
* Yurii Andrieiev (https://github.com/yandrieiev)
* Filip Kucharczyk (https://github.com/Pacu2)
* Eric Timmons (https://github.com/daewok)
* Matthew Simpson (https://github.com/mcsimps2)
* Leonardo Domingues (https://github.com/leodmgs)
* Agustin Barto (https://github.com/abarto)
* Stankiewicz Mateusz (https://github.com/mas15)
* Felix Schultheiß (https://github.com/felix-smashdocs)
* Jan Stein (https://github.com/janste63)
* Timothé Perez (https://github.com/AchilleAsh)
* oleksandr-l5 (https://github.com/oleksandr-l5)
* Ido Shraga (https://github.com/idoshr)

View File

@ -20,20 +20,47 @@ post to the `user group <http://groups.google.com/group/mongoengine-users>`
Supported Interpreters
----------------------
MongoEngine supports CPython 2.7 and newer. Language
features not supported by all interpreters can not be used.
Please also ensure that your code is properly converted by
`2to3 <http://docs.python.org/library/2to3.html>`_ for Python 3 support.
MongoEngine supports CPython 3.5 and newer as well as Pypy3.
Language features not supported by all interpreters can not be used.
Python3 codebase
----------------------
Since 0.20, the codebase is exclusively Python 3.
Earlier versions were exclusively Python2, and were relying on 2to3 to support Python3 installs.
Travis runs the tests against the main Python 3.x versions.
Style Guide
-----------
MongoEngine aims to follow `PEP8 <http://www.python.org/dev/peps/pep-0008/>`_
including 4 space indents. When possible we try to stick to 79 character line
limits. However, screens got bigger and an ORM has a strong focus on
readability and if it can help, we accept 119 as maximum line length, in a
similar way as `django does
<https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
MongoEngine's codebase is auto-formatted with `black <https://github.com/python/black>`_, imports are ordered with `isort <https://pycqa.github.io/isort/>`_
and other tools like flake8 are also used. Those tools will run as part of the CI and will fail in case the code is not formatted properly.
To install all development tools, simply run the following commands:
.. code-block:: console
$ python -m pip install -r requirements-dev.txt
You can install `pre-commit <https://pre-commit.com/>`_ into your git hooks,
to automatically check and fix any formatting issue before creating a
git commit.
To enable ``pre-commit`` simply run:
.. code-block:: console
$ pre-commit install
See the ``.pre-commit-config.yaml`` configuration file for more information
on how it works.
pre-commit will now run upon every commit and will reject anything that doesn't comply.
You can also run all the checks with ``pre-commit run -a``, this is what is used in the CI.
Testing
-------
@ -55,7 +82,7 @@ General Guidelines
should adapt to the breaking change in docs/upgrade.rst.
- Write inline documentation for new classes and methods.
- Write tests and make sure they pass (make sure you have a mongod
running on the default port, then execute ``python setup.py nosetests``
running on the default port, then execute ``python setup.py test``
from the cmd line to run the test suite).
- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions.
You can test various Python and PyMongo versions locally by executing

View File

@ -12,9 +12,8 @@ MongoEngine
.. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master
:target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master
.. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.svg?style=flat
:target: https://landscape.io/github/MongoEngine/mongoengine/master
:alt: Code Health
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
About
=====
@ -26,27 +25,30 @@ an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
Supported MongoDB Versions
==========================
MongoEngine is currently tested against MongoDB v2.4, v2.6, and v3.0. Future
versions should be supported as well, but aren't actively tested at the moment.
Make sure to open an issue or submit a pull request if you experience any
problems with MongoDB v3.2+.
MongoEngine is currently tested against MongoDB v3.4, v3.6 and v4.0. Future versions
should be supported as well, but aren't actively tested at the moment. Make
sure to open an issue or submit a pull request if you experience any problems
with MongoDB version > 4.0.
Installation
============
We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of
`pip <https://pip.pypa.io/>`_. You can then use ``pip install -U mongoengine``.
`pip <https://pip.pypa.io/>`_. You can then use ``python -m pip install -U mongoengine``.
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the
source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and run ``python
setup.py install``.
and thus you can use ``easy_install -U mongoengine``. Another option is
`pipenv <https://docs.pipenv.org/>`_. You can then use ``pipenv install mongoengine``
to both create the virtual environment and install the package. Otherwise, you can
download the source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and
run ``python setup.py install``.
The support for Python2 was dropped with MongoEngine 0.20.0
Dependencies
============
All of the dependencies can easily be installed via `pip <https://pip.pypa.io/>`_.
All of the dependencies can easily be installed via `python -m pip <https://pip.pypa.io/>`_.
At the very least, you'll need these two packages to use MongoEngine:
- pymongo>=2.7.1
- six>=1.10.0
- pymongo>=3.4
If you utilize a ``DateTimeField``, you might also use a more flexible date parser:
@ -56,6 +58,10 @@ If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
- Pillow>=2.0.0
If you need to use signals:
- blinker>=1.3
Examples
========
Some simple examples of what MongoEngine code looks like:
@ -89,12 +95,11 @@ Some simple examples of what MongoEngine code looks like:
# Iterate over all posts using the BlogPost superclass
>>> for post in BlogPost.objects:
... print '===', post.title, '==='
... print('===', post.title, '===')
... if isinstance(post, TextPost):
... print post.content
... print(post.content)
... elif isinstance(post, LinkPost):
... print 'Link:', post.url
... print
... print('Link:', post.url)
...
# Count all blog posts and its subtypes
@ -114,7 +119,8 @@ Some simple examples of what MongoEngine code looks like:
Tests
=====
To run the test suite, ensure you are running a local instance of MongoDB on
the standard port and have ``nose`` installed. Then, run ``python setup.py nosetests``.
the standard port and have ``pytest`` installed. Then, run ``python setup.py test``
or simply ``pytest``.
To run the test suite on every supported Python and PyMongo version, you can
use ``tox``. You'll need to make sure you have each supported Python version
@ -123,20 +129,18 @@ installed in your environment and then:
.. code-block:: shell
# Install tox
$ pip install tox
$ python -m pip install tox
# Run the test suites
$ tox
If you wish to run a subset of tests, use the nosetests convention:
If you wish to run a subset of tests, use the pytest convention:
.. code-block:: shell
# Run all the tests in a particular test file
$ python setup.py nosetests --tests tests/fields/fields.py
$ pytest tests/fields/test_fields.py
# Run only particular test class in that file
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest
# Use the -s option if you want to print some debug statements or use pdb
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest -s
$ pytest tests/fields/test_fields.py::TestField
Community
=========

View File

@ -1,207 +0,0 @@
#!/usr/bin/env python
"""
Simple benchmark comparing PyMongo and MongoEngine.
Sample run on a mid 2015 MacBook Pro (commit b282511):
Benchmarking...
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
2.58979988098
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo write_concern={"w": 0}
1.26657605171
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
8.4351580143
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries without continual assign - MongoEngine
7.20191693306
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True
6.31104588509
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True
6.07083487511
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False
5.97704291344
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False
5.9111430645
"""
import timeit
def main():
print("Benchmarking...")
setup = """
from pymongo import MongoClient
connection = MongoClient()
connection.drop_database('timeit_test')
"""
stmt = """
from pymongo import MongoClient
connection = MongoClient()
db = connection.timeit_test
noddy = db.noddy
for i in range(10000):
example = {'fields': {}}
for j in range(20):
example['fields']['key' + str(j)] = 'value ' + str(j)
noddy.save(example)
myNoddys = noddy.find()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - Pymongo""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
stmt = """
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern
connection = MongoClient()
db = connection.get_database('timeit_test', write_concern=WriteConcern(w=0))
noddy = db.noddy
for i in range(10000):
example = {'fields': {}}
for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j)
noddy.save(example)
myNoddys = noddy.find()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
setup = """
from pymongo import MongoClient
connection = MongoClient()
connection.drop_database('timeit_test')
connection.close()
from mongoengine import Document, DictField, connect
connect('timeit_test')
class Noddy(Document):
fields = DictField()
"""
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save()
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - MongoEngine""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
stmt = """
for i in range(10000):
noddy = Noddy()
fields = {}
for j in range(20):
fields["key"+str(j)] = "value "+str(j)
noddy.fields = fields
noddy.save()
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries without continual assign - MongoEngine""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(write_concern={"w": 0}, cascade=True)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(write_concern={"w": 0}, validate=False, cascade=True)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(validate=False, write_concern={"w": 0})
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(force_insert=True, write_concern={"w": 0}, validate=False)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("""Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""")
t = timeit.Timer(stmt=stmt, setup=setup)
print(t.timeit(1))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,142 @@
from timeit import repeat
import mongoengine
from mongoengine import (
BooleanField,
Document,
EmailField,
EmbeddedDocument,
EmbeddedDocumentField,
IntField,
ListField,
StringField,
)
mongoengine.connect(db="mongoengine_benchmark_test")
def timeit(f, n=10000):
return min(repeat(f, repeat=3, number=n)) / float(n)
def test_basic():
class Book(Document):
name = StringField()
pages = IntField()
tags = ListField(StringField())
is_published = BooleanField()
author_email = EmailField()
Book.drop_collection()
def init_book():
return Book(
name="Always be closing",
pages=100,
tags=["self-help", "sales"],
is_published=True,
author_email="alec@example.com",
)
print("Doc initialization: %.3fus" % (timeit(init_book, 1000) * 10 ** 6))
b = init_book()
print("Doc getattr: %.3fus" % (timeit(lambda: b.name, 10000) * 10 ** 6))
print(
"Doc setattr: %.3fus"
% (timeit(lambda: setattr(b, "name", "New name"), 10000) * 10 ** 6) # noqa B010
)
print("Doc to mongo: %.3fus" % (timeit(b.to_mongo, 1000) * 10 ** 6))
print("Doc validation: %.3fus" % (timeit(b.validate, 1000) * 10 ** 6))
def save_book():
b._mark_as_changed("name")
b._mark_as_changed("tags")
b.save()
print("Save to database: %.3fus" % (timeit(save_book, 100) * 10 ** 6))
son = b.to_mongo()
print(
"Load from SON: %.3fus" % (timeit(lambda: Book._from_son(son), 1000) * 10 ** 6)
)
print(
"Load from database: %.3fus" % (timeit(lambda: Book.objects[0], 100) * 10 ** 6)
)
def create_and_delete_book():
b = init_book()
b.save()
b.delete()
print(
"Init + save to database + delete: %.3fms"
% (timeit(create_and_delete_book, 10) * 10 ** 3)
)
def test_big_doc():
class Contact(EmbeddedDocument):
name = StringField()
title = StringField()
address = StringField()
class Company(Document):
name = StringField()
contacts = ListField(EmbeddedDocumentField(Contact))
Company.drop_collection()
def init_company():
return Company(
name="MongoDB, Inc.",
contacts=[
Contact(name="Contact %d" % x, title="CEO", address="Address %d" % x)
for x in range(1000)
],
)
company = init_company()
print("Big doc to mongo: %.3fms" % (timeit(company.to_mongo, 100) * 10 ** 3))
print("Big doc validation: %.3fms" % (timeit(company.validate, 1000) * 10 ** 3))
company.save()
def save_company():
company._mark_as_changed("name")
company._mark_as_changed("contacts")
company.save()
print("Save to database: %.3fms" % (timeit(save_company, 100) * 10 ** 3))
son = company.to_mongo()
print(
"Load from SON: %.3fms"
% (timeit(lambda: Company._from_son(son), 100) * 10 ** 3)
)
print(
"Load from database: %.3fms"
% (timeit(lambda: Company.objects[0], 100) * 10 ** 3)
)
def create_and_delete_company():
c = init_company()
c.save()
c.delete()
print(
"Init + save to database + delete: %.3fms"
% (timeit(create_and_delete_company, 10) * 10 ** 3)
)
if __name__ == "__main__":
test_basic()
print("-" * 100)
test_big_doc()

161
benchmarks/test_inserts.py Normal file
View File

@ -0,0 +1,161 @@
import timeit
def main():
setup = """
from pymongo import MongoClient
connection = MongoClient()
connection.drop_database('mongoengine_benchmark_test')
"""
stmt = """
from pymongo import MongoClient
connection = MongoClient()
db = connection.mongoengine_benchmark_test
noddy = db.noddy
for i in range(10000):
example = {'fields': {}}
for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j)
noddy.insert_one(example)
myNoddys = noddy.find()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("PyMongo: Creating 10000 dictionaries.")
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
stmt = """
from pymongo import MongoClient, WriteConcern
connection = MongoClient()
db = connection.mongoengine_benchmark_test
noddy = db.noddy.with_options(write_concern=WriteConcern(w=0))
for i in range(10000):
example = {'fields': {}}
for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j)
noddy.insert_one(example)
myNoddys = noddy.find()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print('PyMongo: Creating 10000 dictionaries (write_concern={"w": 0}).')
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
setup = """
from pymongo import MongoClient
connection = MongoClient()
connection.drop_database('mongoengine_benchmark_test')
connection.close()
from mongoengine import Document, DictField, connect
connect("mongoengine_benchmark_test")
class Noddy(Document):
fields = DictField()
"""
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save()
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("MongoEngine: Creating 10000 dictionaries.")
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
stmt = """
for i in range(10000):
noddy = Noddy()
fields = {}
for j in range(20):
fields["key"+str(j)] = "value "+str(j)
noddy.fields = fields
noddy.save()
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print("MongoEngine: Creating 10000 dictionaries (using a single field assignment).")
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(write_concern={"w": 0})
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print('MongoEngine: Creating 10000 dictionaries (write_concern={"w": 0}).')
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(write_concern={"w": 0}, validate=False)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print(
'MongoEngine: Creating 10000 dictionaries (write_concern={"w": 0}, validate=False).'
)
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
stmt = """
for i in range(10000):
noddy = Noddy()
for j in range(20):
noddy.fields["key"+str(j)] = "value "+str(j)
noddy.save(force_insert=True, write_concern={"w": 0}, validate=False)
myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate
"""
print("-" * 100)
print(
'MongoEngine: Creating 10000 dictionaries (force_insert=True, write_concern={"w": 0}, validate=False).'
)
t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s")
if __name__ == "__main__":
main()

View File

@ -33,8 +33,14 @@ clean:
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. Check $(BUILDDIR)/html/index.html"
html-readthedocs:
$(SPHINXBUILD) -T -E -b readthedocs $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo

View File

@ -13,6 +13,7 @@ Documents
.. autoclass:: mongoengine.Document
:members:
:inherited-members:
.. attribute:: objects
@ -21,15 +22,18 @@ Documents
.. autoclass:: mongoengine.EmbeddedDocument
:members:
:inherited-members:
.. autoclass:: mongoengine.DynamicDocument
:members:
:inherited-members:
.. autoclass:: mongoengine.DynamicEmbeddedDocument
:members:
:inherited-members:
.. autoclass:: mongoengine.document.MapReduceDocument
:members:
:members:
.. autoclass:: mongoengine.ValidationError
:members:
@ -71,6 +75,7 @@ Fields
.. autoclass:: mongoengine.fields.StringField
.. autoclass:: mongoengine.fields.URLField
.. autoclass:: mongoengine.fields.EmailField
.. autoclass:: mongoengine.fields.EnumField
.. autoclass:: mongoengine.fields.IntField
.. autoclass:: mongoengine.fields.LongField
.. autoclass:: mongoengine.fields.FloatField
@ -87,7 +92,9 @@ Fields
.. autoclass:: mongoengine.fields.DictField
.. autoclass:: mongoengine.fields.MapField
.. autoclass:: mongoengine.fields.ReferenceField
.. autoclass:: mongoengine.fields.LazyReferenceField
.. autoclass:: mongoengine.fields.GenericReferenceField
.. autoclass:: mongoengine.fields.GenericLazyReferenceField
.. autoclass:: mongoengine.fields.CachedReferenceField
.. autoclass:: mongoengine.fields.BinaryField
.. autoclass:: mongoengine.fields.FileField

View File

@ -1,3 +1,5 @@
=========
Changelog
=========
@ -5,81 +7,300 @@ Changelog
Development
===========
- (Fill this out as you fix issues and develop your features).
- EnumField improvements: now `choices` limits the values of an enum to allow
- Fix deepcopy of EmbeddedDocument #2202
- Fix error when using precision=0 with DecimalField #2535
- Add support for regex and whole word text search query #2568
Changes in 0.23.1
===========
- Bug fix: ignore LazyReferenceFields when clearing _changed_fields #2484
- Improve connection doc #2481
Changes in 0.23.0
=================
- Bugfix: manually setting SequenceField in DynamicDocument doesn't increment the counter #2471
- Add MongoDB 4.2 and 4.4 to CI
- Add support for allowDiskUse on querysets #2468
Changes in 0.22.1
=================
- Declare that Py3.5 is not supported in package metadata #2449
- Moved CI from Travis to Github-Actions
Changes in 0.22.0
=================
- Fix LazyReferenceField dereferencing in embedded documents #2426
- Fix regarding the recent use of Cursor.__spec in .count() that was interfering with mongomock #2425
- Drop support for Python 3.5 by introducing f-strings in the codebase
Changes in 0.21.0
=================
- Bug fix in DynamicDocument which is not parsing known fields in constructor like Document do #2412
- When using pymongo >= 3.7, make use of Collection.count_documents instead of Collection.count
and Cursor.count that got deprecated in pymongo >= 3.7.
This should have a negative impact on performance of count see Issue #2219
- Fix a bug that made the queryset drop the read_preference after clone().
- Remove Py3.5 from CI as it reached EOL and add Python 3.9
- Fix some issues related with db_field/field conflict in constructor #2414
- BREAKING CHANGE: Fix the behavior of Doc.objects.limit(0) which should return all documents (similar to mongodb) #2311
- Bug fix in ListField when updating the first item, it was saving the whole list, instead of
just replacing the first item (as usually done when updating 1 item of the list) #2392
- Add EnumField: ``mongoengine.fields.EnumField``
- Refactoring - Remove useless code related to Document.__only_fields and Queryset.only_fields
- Fix query transformation regarding special operators #2365
- Bug Fix: Document.save() fails when shard_key is not _id #2154
Changes in 0.20.0
=================
- ATTENTION: Drop support for Python2
- Add Mongo 4.0 to Travis
- Fix error when setting a string as a ComplexDateTimeField #2253
- Bump development Status classifier to Production/Stable #2232
- Improve Queryset.get to avoid confusing MultipleObjectsReturned message in case multiple match are found #630
- Fixed a bug causing inaccurate query results, while combining ``__raw__`` and regular filters for the same field #2264
- Add support for the `elemMatch` projection operator in .fields() (e.g BlogPost.objects.fields(elemMatch__comments="test")) #2267
- DictField validate failed without default connection (bug introduced in 0.19.0) #2239
- Remove methods that were deprecated years ago:
- name parameter in Field constructor e.g `StringField(name="...")`, was replaced by db_field
- Queryset.slave_okay() was deprecated since pymongo3
- dropDups was dropped with MongoDB3
- ``Queryset._ensure_indexes`` and ``Queryset.ensure_indexes``, the right method to use is ``Document.ensure_indexes``
- Added pre-commit for development/CI #2212
- Renamed requirements-lint.txt to requirements-dev.txt #2212
- Support for setting ReadConcern #2255
Changes in 0.19.1
=================
- Tests require Pillow < 7.0.0 as it dropped Python2 support
- DEPRECATION: The interface of ``QuerySet.aggregate`` method was changed, it no longer takes an unpacked list of
pipeline steps (*pipeline) but simply takes the pipeline list just like ``pymongo.Collection.aggregate`` does. #2079
Changes in 0.19.0
=================
- BREAKING CHANGE: ``class_check`` and ``read_preference`` keyword arguments are no longer available when filtering a ``QuerySet``. #2112
- Instead of ``Doc.objects(foo=bar, read_preference=...)`` use ``Doc.objects(foo=bar).read_preference(...)``.
- Instead of ``Doc.objects(foo=bar, class_check=False)`` use ``Doc.objects(foo=bar).clear_cls_query(...)``.
- This change also renames the private ``QuerySet._initial_query`` attribute to ``_cls_query``.
- BREAKING CHANGE: Removed the deprecated ``format`` param from ``QuerySet.explain``. #2113
- BREAKING CHANGE: Renamed ``MongoEngineConnectionError`` to ``ConnectionFailure``. #2111
- If you catch/use ``MongoEngineConnectionError`` in your code, you'll have to rename it.
- BREAKING CHANGE: Positional arguments when instantiating a document are no longer supported. #2103
- From now on keyword arguments (e.g. ``Doc(field_name=value)``) are required.
- BREAKING CHANGE: A ``LazyReferenceField`` is now stored in the ``_data`` field of its parent as a ``DBRef``, ``Document``, or ``EmbeddedDocument`` (``ObjectId`` is no longer allowed). #2182
- DEPRECATION: ``Q.empty`` & ``QNode.empty`` are marked as deprecated and will be removed in a next version of MongoEngine. #2210
- Added ability to check if Q or QNode are empty by parsing them to bool.
- Instead of ``Q(name="John").empty`` use ``not Q(name="John")``.
- Fix updating/modifying/deleting/reloading a document that's sharded by a field with ``db_field`` specified. #2125
- Only set no_cursor_timeout when requested (fixes an incompatibility with MongoDB 4.2) #2148
- ``ListField`` now accepts an optional ``max_length`` parameter. #2110
- Improve error message related to InvalidDocumentError #2180
- Added BulkWriteError to replace NotUniqueError which was misleading in bulk write insert #2152
- Added ability to compare Q and Q operations #2204
- Added ability to use a db alias on query_counter #2194
- Added ability to specify collations for querysets with ``Doc.objects.collation`` #2024
- Fix updates of a list field by negative index #2094
- Switch from nosetest to pytest as test runner #2114
- The codebase is now formatted using ``black``. #2109
- Documentation improvements:
- Documented how `pymongo.monitoring` can be used to log all queries issued by MongoEngine to the driver.
Changes in 0.18.2
=================
- Replace deprecated PyMongo v2.x methods with their v3.x equivalents in the ``SequenceField``. #2097
- Various code clarity and documentation improvements.
Changes in 0.18.1
=================
- Fix a bug introduced in 0.18.0 which was causing ``Document.save`` to update all the fields instead of updating only the modified fields. This bug only occurred when using a custom PK. #2082
- Add Python 3.7 to Travis CI. #2058
Changes in 0.18.0
=================
- Drop support for EOL'd MongoDB v2.6, v3.0, and v3.2.
- MongoEngine now requires PyMongo >= v3.4. Travis CI now tests against MongoDB v3.4 v3.6 and PyMongo v3.4 v3.6. #2017 #2066
- Improve performance by avoiding a call to ``to_mongo`` in ``Document.save``. #2049
- Connection/disconnection improvements:
- Expose ``mongoengine.connection.disconnect`` and ``mongoengine.connection.disconnect_all``.
- Fix disconnecting. #566 #1599 #605 #607 #1213 #565
- Improve documentation of ``connect``/``disconnect``.
- Fix issue when using multiple connections to the same mongo with different credentials. #2047
- ``connect`` fails immediately when db name contains invalid characters. #2031 #1718
- Fix the default write concern of ``Document.save`` that was overwriting the connection write concern. #568
- Fix querying on ``List(EmbeddedDocument)`` subclasses fields. #1961 #1492
- Fix querying on ``(Generic)EmbeddedDocument`` subclasses fields. #475
- Fix ``QuerySet.aggregate`` so that it takes limit and skip value into account. #2029
- Generate unique indices for ``SortedListField`` and ``EmbeddedDocumentListFields``. #2020
- BREAKING CHANGE: Changed the behavior of a custom field validator (i.e ``validation`` parameter of a ``Field``). It is now expected to raise a ``ValidationError`` instead of returning ``True``/``False``. #2050
- BREAKING CHANGES (associated with connection/disconnection fixes):
- Calling ``connect`` 2 times with the same alias and different parameter will raise an error (should call ``disconnect`` first).
- ``disconnect`` now clears ``mongoengine.connection._connection_settings``.
- ``disconnect`` now clears the cached attribute ``Document._collection``.
- BREAKING CHANGE: ``EmbeddedDocument.save`` & ``.reload`` no longer exist. #1552
Changes in 0.17.0
=================
- POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (``_cls``, ``_id``) when using ``QuerySet.as_pymongo``. #1976
- Document a BREAKING CHANGE introduced in 0.15.3 and not reported at that time. #1995
- DEPRECATION: ``EmbeddedDocument.save`` & ``.reload`` are marked as deprecated and will be removed in a next version of MongoEngine. #1552
- Fix ``QuerySet.only`` working improperly after using ``QuerySet.count`` of the same instance of a ``QuerySet``.
- Fix ``batch_size`` that was not copied when cloning a ``QuerySet`` object. #2011
- Fix ``InvalidStringData`` error when using ``modify`` on a ``BinaryField``. #1127
- Fix test suite and CI to support MongoDB v3.4. #1445
- Fix reference fields querying the database on each access if value contains orphan DBRefs.
Changes in 0.16.3
=================
- Fix ``$push`` with the ``$position`` operator not working with lists in embedded documents. #1965
Changes in 0.16.2
=================
- Fix ``Document.save`` that fails when called with ``write_concern=None`` (regression of 0.16.1). #1958
Changes in 0.16.1
=================
- Fix ``_cls`` that is not set properly in the ``Document`` constructor (regression). #1950
- Fix a bug in the ``_delta`` method - update of a ``ListField`` depends on an unrelated dynamic field update. #1733
- Remove PyMongo's deprecated ``Collection.save`` method and use ``Collection.insert_one`` instead. #1899
Changes in 0.16.0
=================
- POTENTIAL BREAKING CHANGES:
- ``EmbeddedDocumentField`` will no longer accept references to Document classes in its constructor. #1661
- Get rid of the ``basecls`` parameter from the ``DictField`` constructor (dead code). #1876
- Default value of the ``ComplexDateTime`` field is now ``None`` (and no longer the current datetime). #1368
- Fix an unhashable ``TypeError`` when referencing a ``Document`` with a compound key in an ``EmbeddedDocument``. #1685
- Fix a bug where an ``EmbeddedDocument`` with the same id as its parent would not be tracked for changes. #1768
- Fix the fact that a bulk ``QuerySet.insert`` was not setting primary keys of inserted document instances. #1919
- Fix a bug when referencing an abstract class in a ``ReferenceField``. #1920
- Allow modifications to the document made in ``pre_save_post_validation`` to be taken into account. #1202
- Replace MongoDB v2.4 tests in Travis CI with MongoDB v3.2. #1903
- Fix side effects of using ``QuerySet.no_dereference`` on other documents. #1677
- Fix ``TypeError`` when using lazy Django translation objects as translated choices. #1879
- Improve Python 2-3 codebase compatibility. #1889
- Fix support for changing the default value of the ``ComplexDateTime`` field. #1368
- Improve error message in case an ``EmbeddedDocumentListField`` receives an ``EmbeddedDocument`` instance instead of a list. #1877
- Fix the ``inc`` and ``dec`` operators for the ``DecimalField``. #1517 #1320
- Ignore ``killcursors`` queries in ``query_counter`` context manager. #1869
- Fix the fact that ``query_counter`` was modifying the initial profiling level in case it was != 0. #1870
- Repair the ``no_sub_classes`` context manager + fix the fact that it was swallowing exceptions. #1865
- Fix index creation error that was swallowed by ``hasattr`` under Python 2. #1688
- ``QuerySet.limit`` function behaviour: Passing 0 as parameter will return all the documents in the cursor. #1611
- Bulk insert updates the IDs of the input documents instances. #1919
- Fix a harmless bug related to ``GenericReferenceField`` where modifications in the generic-referenced document were tracked in the parent. #1934
- Improve validation of the ``BinaryField``. #273
- Implement lazy regex compiling in Field classes to improve ``import mongoengine`` performance. #1806
- Update ``GridFSProxy.__str__`` so that it would always print both the filename and grid_id. #710
- Add ``__repr__`` to ``Q`` and ``QCombination`` classes. #1843
- Fix bug in the ``BaseList.__iter__`` operator (was occuring when modifying a BaseList while iterating over it). #1676
- Add a ``DateField``. #513
- Various improvements to the documentation.
- Various code quality improvements.
Changes in 0.15.3
=================
- ``Queryset.update/update_one`` methods now return an ``UpdateResult`` when ``full_result=True`` is provided and no longer a dict. #1491
- Improve ``LazyReferenceField`` and ``GenericLazyReferenceField`` with nested fields. #1704
- Fix the subfield resolve error in ``generic_emdedded_document`` query. #1651 #1652
- Use each modifier only with ``$position``. #1673 #1675
- Fix validation errors in the ``GenericEmbeddedDocumentField``. #1067
- Update cached fields when a ``fields`` argument is given. #1712
- Add a ``db`` parameter to ``register_connection`` for compatibility with ``connect``.
- Use PyMongo v3.x's ``insert_one`` and ``insert_many`` in ``Document.insert``. #1491
- Use PyMongo v3.x's ``update_one`` and ``update_many`` in ``Document.update`` and ``QuerySet.update``. #1491
- Fix how ``reload(fields)`` affects changed fields. #1371
- Fix a bug where the read-only access to the database fails when trying to create indexes. #1338
Changes in 0.15.0
=================
- Add ``LazyReferenceField`` and ``GenericLazyReferenceField``. #1230
Changes in 0.14.1
=================
- Remove ``SemiStrictDict`` and start using a regular dict for ``BaseDocument._data``. #1630
- Add support for the ``$position`` param in the ``$push`` operator. #1566
- Fix ``DateTimeField`` interpreting an empty string as today. #1533
- Add a missing ``__ne__`` method to the ``GridFSProxy`` class. #1632
- Fix ``BaseQuerySet._fields_to_db_fields``. #1553
Changes in 0.14.0
=================
- BREAKING CHANGE: Remove the ``coerce_types`` param from ``QuerySet.as_pymongo``. #1549
- POTENTIAL BREAKING CHANGE: Make ``EmbeddedDocument`` not hashable by default. #1528
- Improve code quality. #1531, #1540, #1541, #1547
Changes in 0.13.0
=================
- POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see
docs/upgrade.rst for details.
- POTENTIAL BREAKING CHANGE: Added Unicode support to the ``EmailField``, see docs/upgrade.rst for details.
Changes in 0.12.0
=================
- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
- Fixed the way `Document.objects.create` works with duplicate IDs #1485
- Fixed connecting to a replica set with PyMongo 2.x #1436
- Fixed using sets in field choices #1481
- Fixed deleting items from a `ListField` #1318
- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
- Fixed behavior of a `dec` update operator #1450
- Added a `rename` update operator #1454
- Added validation for the `db_field` parameter #1448
- Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440
- Fixed the error message displayed when validating unicode URLs #1486
- Raise an error when trying to save an abstract document #1449
- POTENTIAL BREAKING CHANGE: Fix ``limit``/``skip``/``hint``/``batch_size`` chaining. #1476
- POTENTIAL BREAKING CHANGE: Change a public ``QuerySet.clone_into`` method to a private ``QuerySet._clone_into``. #1476
- Fix the way ``Document.objects.create`` works with duplicate IDs. #1485
- Fix connecting to a replica set with PyMongo 2.x. #1436
- Fix using sets in field choices. #1481
- Fix deleting items from a ``ListField``. #1318
- Fix an obscure error message when filtering by ``field__in=non_iterable``. #1237
- Fix behavior of a ``dec`` update operator. #1450
- Add a ``rename`` update operator. #1454
- Add validation for the ``db_field`` parameter. #1448
- Fix the error message displayed when querying an ``EmbeddedDocumentField`` by an invalid value. #1440
- Fix the error message displayed when validating Unicode URLs. #1486
- Raise an error when trying to save an abstract document. #1449
Changes in 0.11.0
=================
- BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428
- BREAKING CHANGE: Dropped Python 2.6 support. #1428
- BREAKING CHANGE: `from mongoengine.base import ErrorClass` won't work anymore for any error from `mongoengine.errors` (e.g. `ValidationError`). Use `from mongoengine.errors import ErrorClass instead`. #1428
- BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334
- Fixed absent rounding for DecimalField when `force_string` is set. #1103
- BREAKING CHANGE: Rename ``ConnectionError`` to ``MongoEngineConnectionError`` since the former is a built-in exception name in Python v3.x. #1428
- BREAKING CHANGE: Drop Python v2.6 support. #1428
- BREAKING CHANGE: ``from mongoengine.base import ErrorClass`` won't work anymore for any error from ``mongoengine.errors`` (e.g. ``ValidationError``). Use ``from mongoengine.errors import ErrorClass instead``. #1428
- BREAKING CHANGE: Accessing a broken reference will raise a ``DoesNotExist`` error. In the past it used to return ``None``. #1334
- Fix absent rounding for the ``DecimalField`` when ``force_string`` is set. #1103
Changes in 0.10.8
=================
- Added support for QuerySet.batch_size (#1426)
- Fixed query set iteration within iteration #1427
- Fixed an issue where specifying a MongoDB URI host would override more information than it should #1421
- Added ability to filter the generic reference field by ObjectId and DBRef #1425
- Fixed delete cascade for models with a custom primary key field #1247
- Added ability to specify an authentication mechanism (e.g. X.509) #1333
- Added support for falsey primary keys (e.g. doc.pk = 0) #1354
- Fixed QuerySet#sum/average for fields w/ explicit db_field #1417
- Fixed filtering by embedded_doc=None #1422
- Added support for cursor.comment #1420
- Fixed doc.get_<field>_display #1419
- Fixed __repr__ method of the StrictDict #1424
- Added a deprecation warning for Python 2.6
- Add support for ``QuerySet.batch_size``. (#1426)
- Fix a query set iteration within an iteration. #1427
- Fix an issue where specifying a MongoDB URI host would override more information than it should. #1421
- Add an ability to filter the ``GenericReferenceField`` by an ``ObjectId`` and a ``DBRef``. #1425
- Fix cascading deletes for models with a custom primary key field. #1247
- Add ability to specify an authentication mechanism (e.g. X.509). #1333
- Add support for falsy primary keys (e.g. ``doc.pk = 0``). #1354
- Fix ``QuerySet.sum/average`` for fields w/ an explicit ``db_field``. #1417
- Fix filtering by ``embedded_doc=None``. #1422
- Add support for ``Cursor.comment``. #1420
- Fix ``doc.get_<field>_display`` methods. #1419
- Fix the ``__repr__`` method of the ``StrictDict`` #1424
- Add a deprecation warning for Python v2.6.
Changes in 0.10.7
=================
- Dropped Python 3.2 support #1390
- Fixed the bug where dynamic doc has index inside a dict field #1278
- Fixed: ListField minus index assignment does not work #1128
- Fixed cascade delete mixing among collections #1224
- Add `signal_kwargs` argument to `Document.save`, `Document.delete` and `BaseQuerySet.insert` to be passed to signals calls #1206
- Raise `OperationError` when trying to do a `drop_collection` on document with no collection set.
- count on ListField of EmbeddedDocumentField fails. #1187
- Fixed long fields stored as int32 in Python 3. #1253
- MapField now handles unicodes keys correctly. #1267
- ListField now handles negative indicies correctly. #1270
- Fixed AttributeError when initializing EmbeddedDocument with positional args. #681
- Fixed no_cursor_timeout error with pymongo 3.0+ #1304
- Replaced map-reduce based QuerySet.sum/average with aggregation-based implementations #1336
- Fixed support for `__` to escape field names that match operators names in `update` #1351
- Fixed BaseDocument#_mark_as_changed #1369
- Added support for pickling QuerySet instances. #1397
- Fixed connecting to a list of hosts #1389
- Fixed a bug where accessing broken references wouldn't raise a DoesNotExist error #1334
- Fixed not being able to specify use_db_field=False on ListField(EmbeddedDocumentField) instances #1218
- Improvements to the dictionary fields docs #1383
- Drop Python 3.2 support #1390
- Fix a bug where a dynamic doc has an index inside a dict field. #1278
- Fix: ``ListField`` minus index assignment does not work. #1128
- Fix cascade delete mixing among collections. #1224
- Add ``signal_kwargs`` argument to ``Document.save``, ``Document.delete`` and ``BaseQuerySet.insert`` to be passed to signals calls. #1206
- Raise ``OperationError`` when trying to do a ``drop_collection`` on document with no collection set.
- Fix a bug where a count on ``ListField`` of ``EmbeddedDocumentField`` fails. #1187
- Fix ``LongField`` values stored as int32 in Python 3. #1253
- ``MapField`` now handles unicode keys correctly. #1267
- ``ListField`` now handles negative indicies correctly. #1270
- Fix an ``AttributeError`` when initializing an ``EmbeddedDocument`` with positional args. #681
- Fix a ``no_cursor_timeout`` error with PyMongo v3.x. #1304
- Replace map-reduce based ``QuerySet.sum/average`` with aggregation-based implementations. #1336
- Fix support for ``__`` to escape field names that match operators' names in ``update``. #1351
- Fix ``BaseDocument._mark_as_changed``. #1369
- Add support for pickling ``QuerySet`` instances. #1397
- Fix connecting to a list of hosts. #1389
- Fix a bug where accessing broken references wouldn't raise a ``DoesNotExist`` error. #1334
- Fix not being able to specify ``use_db_field=False`` on ``ListField(EmbeddedDocumentField)`` instances. #1218
- Improvements to the dictionary field's docs. #1383
Changes in 0.10.6
=================
- Add support for mocking MongoEngine based on mongomock. #1151
- Fixed not being able to run tests on Windows. #1153
- Fix not being able to run tests on Windows. #1153
- Allow creation of sparse compound indexes. #1114
- count on ListField of EmbeddedDocumentField fails. #1187
Changes in 0.10.5
=================
@ -87,12 +308,12 @@ Changes in 0.10.5
Changes in 0.10.4
=================
- SaveConditionError is now importable from the top level package. #1165
- upsert_one method added. #1157
- ``SaveConditionError`` is now importable from the top level package. #1165
- Add a ``QuerySet.upsert_one`` method. #1157
Changes in 0.10.3
=================
- Fix `read_preference` (it had chaining issues with PyMongo 2.x and it didn't work at all with PyMongo 3.x) #1042
- Fix ``read_preference`` (it had chaining issues with PyMongo v2.x and it didn't work at all with PyMongo v3.x). #1042
Changes in 0.10.2
=================
@ -102,16 +323,16 @@ Changes in 0.10.2
Changes in 0.10.1
=================
- Fix infinite recursion with CASCADE delete rules under specific conditions. #1046
- Fix CachedReferenceField bug when loading cached docs as DBRef but failing to save them. #1047
- Fix ignored chained options #842
- Document save's save_condition error raises `SaveConditionError` exception #1070
- Fix Document.reload for DynamicDocument. #1050
- StrictDict & SemiStrictDict are shadowed at init time. #1105
- Fix ListField minus index assignment does not work. #1119
- Remove code that marks field as changed when the field has default but not existed in database #1126
- Remove test dependencies (nose and rednose) from install dependencies list. #1079
- Recursively build query when using elemMatch operator. #1130
- Fix infinite recursion with cascade delete rules under specific conditions. #1046
- Fix ``CachedReferenceField`` bug when loading cached docs as ``DBRef`` but failing to save them. #1047
- Fix ignored chained options. #842
- ``Document.save``'s ``save_condition`` error raises a ``SaveConditionError`` exception. #1070
- Fix ``Document.reload`` for the ``DynamicDocument``. #1050
- ``StrictDict`` & ``SemiStrictDict`` are shadowed at init time. #1105
- Fix ``ListField`` negative index assignment not working. #1119
- Remove code that marks a field as changed when the field has a default value but does not exist in the database. #1126
- Remove test dependencies (nose and rednose) from install dependencies. #1079
- Recursively build a query when using the ``elemMatch`` operator. #1130
- Fix instance back references for lists of embedded documents. #1131
Changes in 0.10.0
@ -122,7 +343,7 @@ Changes in 0.10.0
- Removed get_or_create() deprecated since 0.8.0. #300
- Improve Document._created status when switch collection and db #1020
- Queryset update doesn't go through field validation #453
- Added support for specifying authentication source as option `authSource` in URI. #967
- Added support for specifying authentication source as option ``authSource`` in URI. #967
- Fixed mark_as_changed to handle higher/lower level fields changed. #927
- ListField of embedded docs doesn't set the _instance attribute when iterating over it #914
- Support += and *= for ListField #595
@ -138,7 +359,7 @@ Changes in 0.10.0
- Fixes some internal _id handling issue. #961
- Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652
- Capped collection multiple of 256. #1011
- Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods.
- Added ``BaseQuerySet.aggregate_sum`` and ``BaseQuerySet.aggregate_average`` methods.
- Fix for delete with write_concern {'w': 0}. #1008
- Allow dynamic lookup for more than two parts. #882
- Added support for min_distance on geo queries. #831
@ -147,10 +368,10 @@ Changes in 0.10.0
Changes in 0.9.0
================
- Update FileField when creating a new file #714
- Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826
- Added ``EmbeddedDocumentListField`` for Lists of Embedded Documents. #826
- ComplexDateTimeField should fall back to None when null=True #864
- Request Support for $min, $max Field update operators #863
- `BaseDict` does not follow `setdefault` #866
- ``BaseDict`` does not follow ``setdefault`` #866
- Add support for $type operator # 766
- Fix tests for pymongo 2.8+ #877
- No module named 'django.utils.importlib' (Django dev) #872
@ -171,13 +392,13 @@ Changes in 0.9.0
- Stop ensure_indexes running on a secondaries unless connection is through mongos #746
- Not overriding default values when loading a subset of fields #399
- Saving document doesn't create new fields in existing collection #620
- Added `Queryset.aggregate` wrapper to aggregation framework #703
- Added ``Queryset.aggregate`` wrapper to aggregation framework #703
- Added support to show original model fields on to_json calls instead of db_field #697
- Added Queryset.search_text to Text indexes searchs #700
- Fixed tests for Django 1.7 #696
- Follow ReferenceFields in EmbeddedDocuments with select_related #690
- Added preliminary support for text indexes #680
- Added `elemMatch` operator as well - `match` is too obscure #653
- Added ``elemMatch`` operator as well - ``match`` is too obscure #653
- Added support for progressive JPEG #486 #548
- Allow strings to be used in index creation #675
- Fixed EmbeddedDoc weakref proxy issue #592
@ -213,11 +434,11 @@ Changes in 0.9.0
- Increase email field length to accommodate new TLDs #726
- index_cls is ignored when deciding to set _cls as index prefix #733
- Make 'db' argument to connection optional #737
- Allow atomic update for the entire `DictField` #742
- Allow atomic update for the entire ``DictField`` #742
- Added MultiPointField, MultiLineField, MultiPolygonField
- Fix multiple connections aliases being rewritten #748
- Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791
- Make `in_bulk()` respect `no_dereference()` #775
- Make ``in_bulk()`` respect ``no_dereference()`` #775
- Handle None from model __str__; Fixes #753 #754
- _get_changed_fields fix for embedded documents with id field. #925
@ -271,18 +492,15 @@ Changes in 0.8.4
Changes in 0.8.3
================
- Fixed EmbeddedDocuments with `id` also storing `_id` (#402)
- Fixed EmbeddedDocuments with ``id`` also storing ``_id`` (#402)
- Added get_proxy_object helper to filefields (#391)
- Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365)
- Fixed sum and average mapreduce dot notation support (#375, #376, #393)
- Fixed as_pymongo to return the id (#386)
- Document.select_related() now respects `db_alias` (#377)
- Document.select_related() now respects ``db_alias`` (#377)
- Reload uses shard_key if applicable (#384)
- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396)
**Potential breaking change:** http://docs.mongoengine.org/en/latest/upgrade.html#to-0-8-3
- Fixed pickling dynamic documents `_dynamic_fields` (#387)
- Fixed pickling dynamic documents ``_dynamic_fields`` (#387)
- Fixed ListField setslice and delslice dirty tracking (#390)
- Added Django 1.5 PY3 support (#392)
- Added match ($elemMatch) support for EmbeddedDocuments (#379)
@ -323,7 +541,7 @@ Changes in 0.8.0
================
- Fixed querying ReferenceField custom_id (#317)
- Fixed pickle issues with collections (#316)
- Added `get_next_value` preview for SequenceFields (#319)
- Added ``get_next_value`` preview for SequenceFields (#319)
- Added no_sub_classes context manager and queryset helper (#312)
- Querysets now utilises a local cache
- Changed __len__ behaviour in the queryset (#247, #311)
@ -352,7 +570,7 @@ Changes in 0.8.0
- Updated connection to use MongoClient (#262, #274)
- Fixed db_alias and inherited Documents (#143)
- Documentation update for document errors (#124)
- Deprecated `get_or_create` (#35)
- Deprecated ``get_or_create`` (#35)
- Updated inheritable objects created by upsert now contain _cls (#118)
- Added support for creating documents with embedded documents in a single operation (#6)
- Added to_json and from_json to Document (#1)
@ -473,7 +691,7 @@ Changes in 0.7.0
- Fixed UnboundLocalError in composite index with pk field (#88)
- Updated ReferenceField's to optionally store ObjectId strings
this will become the default in 0.8 (#89)
- Added FutureWarning - save will default to `cascade=False` in 0.8
- Added FutureWarning - save will default to ``cascade=False`` in 0.8
- Added example of indexing embedded document fields (#75)
- Fixed ImageField resizing when forcing size (#80)
- Add flexibility for fields handling bad data (#78)
@ -569,7 +787,7 @@ Changes in 0.6.8
================
- Fixed FileField losing reference when no default set
- Removed possible race condition from FileField (grid_file)
- Added assignment to save, can now do: `b = MyDoc(**kwargs).save()`
- Added assignment to save, can now do: ``b = MyDoc(**kwargs).save()``
- Added support for pull operations on nested EmbeddedDocuments
- Added support for choices with GenericReferenceFields
- Added support for choices with GenericEmbeddedDocumentFields
@ -584,7 +802,7 @@ Changes in 0.6.7
- Fixed indexing on '_id' or 'pk' or 'id'
- Invalid data from the DB now raises a InvalidDocumentError
- Cleaned up the Validation Error - docs and code
- Added meta `auto_create_index` so you can disable index creation
- Added meta ``auto_create_index`` so you can disable index creation
- Added write concern options to inserts
- Fixed typo in meta for index options
- Bug fix Read preference now passed correctly
@ -625,7 +843,6 @@ Changes in 0.6.1
Changes in 0.6
==============
- Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7
- Added support for covered indexes when inheritance is off
- No longer always upsert on save for items with a '_id'
@ -850,7 +1067,6 @@ Changes in v0.1.3
querying takes place
- A few minor bugfixes
Changes in v0.1.2
=================
- Query values may be processed before before being used in queries
@ -859,7 +1075,6 @@ Changes in v0.1.2
- Added ``BooleanField``
- Added ``Document.reload()`` method
Changes in v0.1.1
=================
- Documents may now use capped collections

View File

@ -1,16 +1,19 @@
from mongoengine import *
connect('tumblelog')
connect("tumblelog")
class Comment(EmbeddedDocument):
content = StringField()
name = StringField(max_length=120)
class User(Document):
email = StringField(required=True)
first_name = StringField(max_length=50)
last_name = StringField(max_length=50)
class Post(Document):
title = StringField(max_length=120, required=True)
author = ReferenceField(User)
@ -18,54 +21,57 @@ class Post(Document):
comments = ListField(EmbeddedDocumentField(Comment))
# bugfix
meta = {'allow_inheritance': True}
meta = {"allow_inheritance": True}
class TextPost(Post):
content = StringField()
class ImagePost(Post):
image_path = StringField()
class LinkPost(Post):
link_url = StringField()
Post.drop_collection()
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
john = User(email="jdoe@example.com", first_name="John", last_name="Doe")
john.save()
post1 = TextPost(title='Fun with MongoEngine', author=john)
post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
post1.tags = ['mongodb', 'mongoengine']
post1 = TextPost(title="Fun with MongoEngine", author=john)
post1.content = "Took a look at MongoEngine today, looks pretty cool."
post1.tags = ["mongodb", "mongoengine"]
post1.save()
post2 = LinkPost(title='MongoEngine Documentation', author=john)
post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs'
post2.tags = ['mongoengine']
post2 = LinkPost(title="MongoEngine Documentation", author=john)
post2.link_url = "http://tractiondigital.com/labs/mongoengine/docs"
post2.tags = ["mongoengine"]
post2.save()
print 'ALL POSTS'
print
print("ALL POSTS")
print()
for post in Post.objects:
print post.title
#print '=' * post.title.count()
print "=" * 20
print(post.title)
# print '=' * post.title.count()
print("=" * 20)
if isinstance(post, TextPost):
print post.content
print(post.content)
if isinstance(post, LinkPost):
print 'Link:', post.link_url
print("Link:", post.link_url)
print
print
print()
print()
print 'POSTS TAGGED \'MONGODB\''
print
for post in Post.objects(tags='mongodb'):
print post.title
print
print("POSTS TAGGED 'MONGODB'")
print()
for post in Post.objects(tags="mongodb"):
print(post.title)
print()
num_posts = Post.objects(tags='mongodb').count()
print 'Found %d posts with tag "mongodb"' % num_posts
num_posts = Post.objects(tags="mongodb").count()
print('Found %d posts with tag "mongodb"' % num_posts)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# MongoEngine documentation build configuration file, created by
# sphinx-quickstart on Sun Nov 22 18:14:13 2009.
@ -11,7 +10,8 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import os
import sys
import sphinx_rtd_theme
@ -20,29 +20,29 @@ import mongoengine
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo']
extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "readthedocs_ext.readthedocs"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8'
# source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'MongoEngine'
copyright = u'2009, MongoEngine Authors'
project = "MongoEngine"
copyright = "2009, MongoEngine Authors" # noqa: A001
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@ -55,68 +55,66 @@ release = mongoengine.get_version()
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
exclude_trees = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'canonical_url': 'http://docs.mongoengine.org/en/latest/'
}
html_theme_options = {"canonical_url": "http://docs.mongoengine.org/en/latest/"}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@ -126,11 +124,11 @@ html_favicon = "favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
#html_static_path = ['_static']
# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
@ -138,69 +136,68 @@ html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['globaltoc.html', 'searchbox.html'],
'**': ['localtoc.html', 'relations.html', 'searchbox.html']
"index": ["globaltoc.html", "searchbox.html"],
"**": ["localtoc.html", "relations.html", "searchbox.html"],
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'MongoEnginedoc'
htmlhelp_basename = "MongoEnginedoc"
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
latex_paper_size = 'a4'
latex_paper_size = "a4"
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'MongoEngine.tex', 'MongoEngine Documentation',
'Ross Lawley', 'manual'),
("index", "MongoEngine.tex", "MongoEngine Documentation", "Ross Lawley", "manual")
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
# latex_use_modindex = True
autoclass_content = 'both'
autoclass_content = "both"

View File

@ -13,7 +13,7 @@ Help Wanted!
The MongoEngine team is looking for help contributing and maintaining a new
Django extension for MongoEngine! If you have Django experience and would like
to help contribute to the project, please get in touch on the
`mailing list <http://groups.google.com/group/mongoengine-users>`_ or by
to help contribute to the project, please get in touch on the
`mailing list <http://groups.google.com/group/mongoengine-users>`_ or by
simply contributing on
`GitHub <https://github.com/MongoEngine/django-mongoengine>`_.

12
docs/faq.rst Normal file
View File

@ -0,0 +1,12 @@
==========================
Frequently Asked Questions
==========================
Does MongoEngine support asynchronous drivers (Motor, TxMongo)?
---------------------------------------------------------------
No, MongoEngine is exclusively based on PyMongo and isn't designed to support other driver.
If this is a requirement for your project, check the alternative: `uMongo`_ and `MotorEngine`_.
.. _uMongo: https://umongo.readthedocs.io/
.. _MotorEngine: https://motorengine.readthedocs.io/

View File

@ -4,33 +4,76 @@
Connecting to MongoDB
=====================
To connect to a running instance of :program:`mongod`, use the
:func:`~mongoengine.connect` function. The first argument is the name of the
database to connect to::
Connections in MongoEngine are registered globally and are identified with aliases.
If no ``alias`` is provided during the connection, it will use "default" as alias.
To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect`
function. The first argument is the name of the database to connect to::
from mongoengine import connect
connect('project1')
By default, MongoEngine assumes that the :program:`mongod` instance is running
on **localhost** on port **27017**. If MongoDB is running elsewhere, you should
provide the :attr:`host` and :attr:`port` arguments to
:func:`~mongoengine.connect`::
on **localhost** on port **27017**.
connect('project1', host='192.168.1.35', port=12345)
If MongoDB is running elsewhere, you need to provide details on how to connect. There are two ways of
doing this. Using a connection string in URI format (**this is the preferred method**) or individual attributes
provided as keyword arguments.
If the database requires authentication, :attr:`username` and :attr:`password`
arguments should be provided::
Connect with URI string
=======================
connect('project1', username='webapp', password='pwd123')
When using a connection string in URI format you should specify the connection details
as the :attr:`host` to :func:`~mongoengine.connect`. In a web application context for instance, the URI
is typically read from the config file::
URI style connections are also supported -- just supply the URI as
the :attr:`host` to
:func:`~mongoengine.connect`::
connect(host="mongodb://127.0.0.1:27017/my_db")
connect('project1', host='mongodb://localhost/database_name')
If the database requires authentication, you can specify it in the
URI. As each database can have its own users configured, you need to tell MongoDB
where to look for the user you are working with, that's what the ``?authSource=admin`` bit
of the MongoDB connection string is for::
# Connects to 'my_db' database by authenticating
# with given credentials against the 'admin' database (by default as authSource isn't provided)
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db")
# Equivalent to previous connection but explicitly states that
# it should use admin as the authentication source database
connect(host="mongodb://my_user:my_password@hostname:port/my_db?authSource=admin")
# Connects to 'my_db' database by authenticating
# with given credentials against that same database
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=my_db")
The URI string can also be used to configure advanced parameters like ssl, replicaSet, etc. For more
information or example about URI string, you can refer to the `official doc <https://docs.mongodb.com/manual/reference/connection-string/>`_::
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=admin&ssl=true&replicaSet=globaldb")
.. note:: URI containing SRV records (e.g "mongodb+srv://server.example.com/") can be used as well
Connect with keyword attributes
===============================
The second option for specifying the connection details is to provide the information as keyword
attributes to :func:`~mongoengine.connect`::
connect('my_db', host='127.0.0.1', port=27017)
If the database requires authentication, :attr:`username`, :attr:`password`
and :attr:`authentication_source` arguments should be provided::
connect('my_db', username='my_user', password='my_password', authentication_source='admin')
The set of attributes that :func:`~mongoengine.connect` recognizes includes but is not limited to:
:attr:`host`, :attr:`port`, :attr:`read_preference`, :attr:`username`, :attr:`password`, :attr:`authentication_source`, :attr:`authentication_mechanism`,
:attr:`replicaset`, :attr:`tls`, etc. Most of the parameters accepted by `pymongo.MongoClient <https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`_
can be used with :func:`~mongoengine.connect` and will simply be forwarded when instantiating the `pymongo.MongoClient`.
.. note:: Database, username and password from URI string overrides
corresponding parameters in :func:`~mongoengine.connect`: ::
corresponding parameters in :func:`~mongoengine.connect`, this should
obviously be avoided: ::
connect(
db='test',
@ -39,25 +82,19 @@ the :attr:`host` to
host='mongodb://admin:qwerty@localhost/production'
)
will establish connection to ``production`` database using
``admin`` username and ``qwerty`` password.
will establish connection to ``production`` database using ``admin`` username and ``qwerty`` password.
Replica Sets
============
.. note:: Calling :func:`~mongoengine.connect` without argument will establish
a connection to the "test" database by default
MongoEngine supports connecting to replica sets::
Read Preferences
================
from mongoengine import connect
# Regular connect
connect('dbname', replicaset='rs-name')
# MongoDB URI-style connect
connect(host='mongodb://localhost/dbname?replicaSet=rs-name')
Read preferences are supported through the connection or via individual
As stated above, Read preferences are supported through the connection but also via individual
queries by passing the read_preference ::
from pymongo import ReadPreference
Bar.objects().read_preference(ReadPreference.PRIMARY)
Bar.objects(read_preference=ReadPreference.PRIMARY)
@ -71,28 +108,61 @@ is used.
In the background this uses :func:`~mongoengine.register_connection` to
store the data and you can register all aliases up front if required.
Individual documents can also support multiple databases by providing a
Documents defined in different database
---------------------------------------
Individual documents can be attached to different databases by providing a
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef`
objects to point across databases and collections. Below is an example schema,
using 3 different databases to store data::
connect(alias='user-db-alias', db='user-db')
connect(alias='book-db-alias', db='book-db')
connect(alias='users-books-db-alias', db='users-books-db')
class User(Document):
name = StringField()
meta = {'db_alias': 'user-db'}
meta = {'db_alias': 'user-db-alias'}
class Book(Document):
name = StringField()
meta = {'db_alias': 'book-db'}
meta = {'db_alias': 'book-db-alias'}
class AuthorBooks(Document):
author = ReferenceField(User)
book = ReferenceField(Book)
meta = {'db_alias': 'users-books-db'}
meta = {'db_alias': 'users-books-db-alias'}
Disconnecting an existing connection
------------------------------------
The function :func:`~mongoengine.disconnect` can be used to
disconnect a particular connection. This can be used to change a
connection globally::
from mongoengine import connect, disconnect
connect('a_db', alias='db1')
class User(Document):
name = StringField()
meta = {'db_alias': 'db1'}
disconnect(alias='db1')
connect('another_db', alias='db1')
.. note:: Calling :func:`~mongoengine.disconnect` without argument
will disconnect the "default" connection
.. note:: Since connections gets registered globally, it is important
to use the `disconnect` function from MongoEngine and not the
`disconnect()` method of an existing connection (pymongo.MongoClient)
.. note:: :class:`~mongoengine.Document` are caching the pymongo collection.
using `disconnect` ensures that it gets cleaned as well
Context Managers
================
Sometimes you may want to switch the database or collection to query against.
@ -119,7 +189,7 @@ access to the same User document across databases::
Switch Collection
-----------------
The :class:`~mongoengine.context_managers.switch_collection` context manager
The :func:`~mongoengine.context_managers.switch_collection` context manager
allows you to change the collection for a given class allowing quick and easy
access to the same Group document across collection::

View File

@ -22,11 +22,13 @@ objects** as class attributes to the document class::
class Page(Document):
title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.now)
date_modified = DateTimeField(default=datetime.datetime.utcnow)
As BSON (the binary format for storing data in mongodb) is order dependent,
documents are serialized based on their field order.
.. _dynamic-document-schemas:
Dynamic document schemas
========================
One of the benefits of MongoDB is dynamic schemas for a collection, whilst data
@ -76,17 +78,21 @@ are as follows:
* :class:`~mongoengine.fields.EmailField`
* :class:`~mongoengine.fields.EmbeddedDocumentField`
* :class:`~mongoengine.fields.EmbeddedDocumentListField`
* :class:`~mongoengine.fields.EnumField`
* :class:`~mongoengine.fields.FileField`
* :class:`~mongoengine.fields.FloatField`
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
* :class:`~mongoengine.fields.GenericReferenceField`
* :class:`~mongoengine.fields.GenericLazyReferenceField`
* :class:`~mongoengine.fields.GeoPointField`
* :class:`~mongoengine.fields.ImageField`
* :class:`~mongoengine.fields.IntField`
* :class:`~mongoengine.fields.ListField`
* :class:`~mongoengine.fields.LongField`
* :class:`~mongoengine.fields.MapField`
* :class:`~mongoengine.fields.ObjectIdField`
* :class:`~mongoengine.fields.ReferenceField`
* :class:`~mongoengine.fields.LazyReferenceField`
* :class:`~mongoengine.fields.SequenceField`
* :class:`~mongoengine.fields.SortedListField`
* :class:`~mongoengine.fields.StringField`
@ -153,7 +159,7 @@ arguments can be set on all fields:
An iterable (e.g. list, tuple or set) of choices to which the value of this
field should be limited.
Can be either be a nested tuples of value (stored in mongo) and a
Can either be nested tuples of value (stored in mongo) and a
human readable key ::
SIZE = (('S', 'Small'),
@ -173,6 +179,21 @@ arguments can be set on all fields:
class Shirt(Document):
size = StringField(max_length=3, choices=SIZE)
:attr:`validation` (Optional)
A callable to validate the value of the field.
The callable takes the value as parameter and should raise a ValidationError
if validation fails
e.g ::
def _not_empty(val):
if not val:
raise ValidationError('value can not be empty')
class Person(Document):
name = StringField(validation=_not_empty)
:attr:`**kwargs` (Optional)
You can supply additional metadata as arbitrary additional keyword
arguments. You can not override existing attributes, however. Common
@ -212,6 +233,9 @@ document class as the first argument::
comment2 = Comment(content='Nice article!')
page = Page(comments=[comment1, comment2])
Embedded documents can also leverage the flexibility of :ref:`dynamic-document-schemas:`
by inheriting :class:`~mongoengine.DynamicEmbeddedDocument`.
Dictionary Fields
-----------------
Often, an embedded document may be used instead of a dictionary generally
@ -224,7 +248,7 @@ store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate
user = ReferenceField(User)
answers = DictField()
survey_response = SurveyResponse(date=datetime.now(), user=request.user)
survey_response = SurveyResponse(date=datetime.utcnow(), user=request.user)
response_form = ResponseForm(request.POST)
survey_response.answers = response_form.cleaned_data()
survey_response.save()
@ -271,12 +295,12 @@ as the constructor's argument::
content = StringField()
.. _one-to-many-with-listfields:
.. _many-to-many-with-listfields:
One to Many with ListFields
Many to Many with ListFields
'''''''''''''''''''''''''''
If you are implementing a one to many relationship via a list of references,
If you are implementing a many to many relationship via a list of references,
then the references are stored as DBRefs and to query you need to pass an
instance of the object to the query::
@ -317,7 +341,6 @@ supplying the :attr:`reverse_delete_rule` attributes on the
:class:`ReferenceField` definition, like this::
class ProfilePage(Document):
...
employee = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE)
The declaration in this example means that when an :class:`Employee` object is
@ -334,7 +357,7 @@ Its value can take any of the following constants:
Deletion is denied if there still exist references to the object being
deleted.
:const:`mongoengine.NULLIFY`
Any object's fields still referring to the object being deleted are removed
Any object's fields still referring to the object being deleted are set to None
(using MongoDB's "unset" operation), effectively nullifying the relationship.
:const:`mongoengine.CASCADE`
Any object containing fields that are referring to the object being deleted
@ -408,28 +431,15 @@ either a single field name, or a list or tuple of field names::
first_name = StringField()
last_name = StringField(unique_with='first_name')
Skipping Document validation on save
------------------------------------
You can also skip the whole document validation process by setting
``validate=False`` when calling the :meth:`~mongoengine.document.Document.save`
method::
class Recipient(Document):
name = StringField()
email = EmailField()
recipient = Recipient(name='admin', email='root@localhost')
recipient.save() # will raise a ValidationError while
recipient.save(validate=False) # won't
Document collections
====================
Document classes that inherit **directly** from :class:`~mongoengine.Document`
will have their own **collection** in the database. The name of the collection
is by default the name of the class, converted to lowercase (so in the example
above, the collection would be called `page`). If you need to change the name
of the collection (e.g. to use MongoEngine with an existing database), then
create a class dictionary attribute called :attr:`meta` on your document, and
is by default the name of the class converted to snake_case (e.g if your Document class
is named `CompanyUser`, the corresponding collection would be `company_user`). If you need
to change the name of the collection (e.g. to use MongoEngine with an existing database),
then create a class dictionary attribute called :attr:`meta` on your document, and
set :attr:`collection` to the name of the collection that you want your
document class to use::
@ -467,7 +477,7 @@ dictionary containing a full index definition.
A direction may be specified on fields by prefixing the field name with a
**+** (for ascending) or a **-** sign (for descending). Note that direction
only matters on multi-field indexes. Text indexes may be specified by prefixing
only matters on compound indexes. Text indexes may be specified by prefixing
the field name with a **$**. Hashed indexes may be specified by prefixing
the field name with a **#**::
@ -478,19 +488,21 @@ the field name with a **#**::
created = DateTimeField()
meta = {
'indexes': [
'title',
'title', # single-field index
'$title', # text index
'#title', # hashed index
('title', '-rating'),
('category', '_cls'),
('title', '-rating'), # compound index
('category', '_cls'), # compound index
{
'fields': ['created'],
'expireAfterSeconds': 3600
'expireAfterSeconds': 3600 # ttl index
}
]
}
If a dictionary is passed then the following options are available:
If a dictionary is passed then additional options become available. Valid options include,
but are not limited to:
:attr:`fields` (Default: None)
The fields to index. Specified in the same format as described above.
@ -511,8 +523,15 @@ If a dictionary is passed then the following options are available:
Allows you to automatically expire data from a collection by setting the
time in seconds to expire the a field.
:attr:`name` (Optional)
Allows you to specify a name for the index
:attr:`collation` (Optional)
Allows to create case insensitive indexes (MongoDB v3.4+ only)
.. note::
Additional options are forwarded as **kwargs to pymongo's create_index method.
Inheritance adds extra fields indices see: :ref:`document-inheritance`.
Global index default options
@ -524,15 +543,15 @@ There are a few top level defaults for all indexes that can be set::
title = StringField()
rating = StringField()
meta = {
'index_options': {},
'index_opts': {},
'index_background': True,
'index_drop_dups': True,
'index_cls': False
'index_cls': False,
'auto_create_index': True,
}
:attr:`index_options` (Optional)
Set any default index options - see the `full options list <http://docs.mongodb.org/manual/reference/method/db.collection.ensureIndex/#db.collection.ensureIndex>`_
:attr:`index_opts` (Optional)
Set any default index options - see the `full options list <https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#db.collection.createIndex>`_
:attr:`index_background` (Optional)
Set the default value for if an index should be indexed in the background
@ -540,11 +559,11 @@ There are a few top level defaults for all indexes that can be set::
:attr:`index_cls` (Optional)
A way to turn off a specific index for _cls.
:attr:`index_drop_dups` (Optional)
Set the default value for if an index should drop duplicates
.. note:: Since MongoDB 3.0 drop_dups is not supported anymore. Raises a Warning
and has no effect
:attr:`auto_create_index` (Optional)
When this is True (default), MongoEngine will ensure that the correct
indexes exist in MongoDB each time a command is run. This can be disabled
in systems where indexes are managed separately. Disabling this will improve
performance.
Compound Indexes and Indexing sub documents
@ -609,8 +628,8 @@ point. To create a geospatial index you must prefix the field with the
],
}
Time To Live indexes
--------------------
Time To Live (TTL) indexes
--------------------------
A special index type that allows you to automatically expire data from a
collection after a given period. See the official
@ -618,7 +637,7 @@ collection after a given period. See the official
documentation for more information. A common usecase might be session data::
class Session(Document):
created = DateTimeField(default=datetime.now)
created = DateTimeField(default=datetime.utcnow)
meta = {
'indexes': [
{'fields': ['created'], 'expireAfterSeconds': 3600}
@ -681,11 +700,16 @@ subsequent calls to :meth:`~mongoengine.queryset.QuerySet.order_by`. ::
Shard keys
==========
If your collection is sharded, then you need to specify the shard key as a tuple,
using the :attr:`shard_key` attribute of :attr:`~mongoengine.Document.meta`.
This ensures that the shard key is sent with the query when calling the
:meth:`~mongoengine.document.Document.save` or
:meth:`~mongoengine.document.Document.update` method on an existing
If your collection is sharded by multiple keys, then you can improve shard
routing (and thus the performance of your application) by specifying the shard
key, using the :attr:`shard_key` attribute of
:attr:`~mongoengine.Document.meta`. The shard key should be defined as a tuple.
This ensures that the full shard key is sent with the query when calling
methods such as :meth:`~mongoengine.document.Document.save`,
:meth:`~mongoengine.document.Document.update`,
:meth:`~mongoengine.document.Document.modify`, or
:meth:`~mongoengine.document.Document.delete` on an existing
:class:`~mongoengine.Document` instance::
class LogEntry(Document):
@ -695,7 +719,8 @@ This ensures that the shard key is sent with the query when calling the
data = StringField()
meta = {
'shard_key': ('machine', 'timestamp',)
'shard_key': ('machine', 'timestamp'),
'indexes': ('machine', 'timestamp'),
}
.. _document-inheritance:
@ -705,7 +730,7 @@ Document inheritance
To create a specialised type of a :class:`~mongoengine.Document` you have
defined, you may subclass it and add any extra fields or methods you may need.
As this is new class is not a direct subclass of
As this new class is not a direct subclass of
:class:`~mongoengine.Document`, it will not be stored in its own collection; it
will use the same collection as its superclass uses. This allows for more
convenient and efficient retrieval of related documents -- all you need do is
@ -725,6 +750,30 @@ document.::
.. note:: From 0.8 onwards :attr:`allow_inheritance` defaults
to False, meaning you must set it to True to use inheritance.
Setting :attr:`allow_inheritance` to True should also be used in
:class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it
When it comes to querying using :attr:`.objects()`, querying `Page.objects()` will query
both `Page` and `DatedPage` whereas querying `DatedPage` will only query the `DatedPage` documents.
Behind the scenes, MongoEngine deals with inheritance by adding a :attr:`_cls` attribute that contains
the class name in every documents. When a document is loaded, MongoEngine checks
it's :attr:`_cls` attribute and use that class to construct the instance.::
Page(title='a funky title').save()
DatedPage(title='another title', date=datetime.utcnow()).save()
print(Page.objects().count()) # 2
print(DatedPage.objects().count()) # 1
# print documents in their native form
# we remove 'id' to avoid polluting the output with unnecessary detail
qs = Page.objects.exclude('id').as_pymongo()
print(list(qs))
# [
# {'_cls': u 'Page', 'title': 'a funky title'},
# {'_cls': u 'Page.DatedPage', 'title': u 'another title', 'date': datetime.datetime(2019, 12, 13, 20, 16, 59, 993000)}
# ]
Working with existing data
--------------------------
As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and

View File

@ -41,34 +41,6 @@ already exist, then any changes will be updated atomically. For example::
.. seealso::
:ref:`guide-atomic-updates`
Pre save data validation and cleaning
-------------------------------------
MongoEngine allows you to create custom cleaning rules for your documents when
calling :meth:`~mongoengine.Document.save`. By providing a custom
:meth:`~mongoengine.Document.clean` method you can do any pre validation / data
cleaning.
This might be useful if you want to ensure a default value based on other
document values for example::
class Essay(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
pub_date = DateTimeField()
def clean(self):
"""Ensures that only published essays have a `pub_date` and
automatically sets the pub_date if published and not set"""
if self.status == 'Draft' and self.pub_date is not None:
msg = 'Draft entries should not have a publication date.'
raise ValidationError(msg)
# Set the pub_date for published items if not set.
if self.status == 'Published' and self.pub_date is None:
self.pub_date = datetime.now()
.. note::
Cleaning is only called if validation is turned on and when calling
:meth:`~mongoengine.Document.save`.
Cascading Saves
---------------
If your document contains :class:`~mongoengine.fields.ReferenceField` or

View File

@ -2,16 +2,15 @@
GridFS
======
.. versionadded:: 0.4
Writing
-------
GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field
object. This field acts as a file-like object and provides a couple of
different ways of inserting and retrieving data. Arbitrary metadata such as
content type can also be stored alongside the files. In the following example,
a document is created to store details about animals, including a photo::
content type can also be stored alongside the files. The object returned when accessing a
FileField is a proxy to `Pymongo's GridFS <https://api.mongodb.com/python/current/examples/gridfs.html#gridfs-example>`_
In the following example, a document is created to store details about animals, including a photo::
class Animal(Document):
genus = StringField()
@ -20,8 +19,8 @@ a document is created to store details about animals, including a photo::
marmot = Animal(genus='Marmota', family='Sciuridae')
marmot_photo = open('marmot.jpg', 'rb')
marmot.photo.put(marmot_photo, content_type = 'image/jpeg')
with open('marmot.jpg', 'rb') as fd:
marmot.photo.put(fd, content_type = 'image/jpeg')
marmot.save()
Retrieval
@ -34,6 +33,20 @@ field. The file can also be retrieved just as easily::
photo = marmot.photo.read()
content_type = marmot.photo.content_type
.. note:: If you need to read() the content of a file multiple times, you'll need to "rewind"
the file-like object using `seek`::
marmot = Animal.objects(genus='Marmota').first()
content1 = marmot.photo.read()
assert content1 != ""
content2 = marmot.photo.read() # will be empty
assert content2 == ""
marmot.photo.seek(0) # rewind the file by setting the current position of the cursor in the file to 0
content3 = marmot.photo.read()
assert content3 == content1
Streaming
---------
@ -53,7 +66,8 @@ Deletion
Deleting stored files is achieved with the :func:`delete` method::
marmot.photo.delete()
marmot.photo.delete() # Deletes the GridFS document
marmot.save() # Saves the GridFS reference (being None) contained in the marmot instance
.. warning::
@ -71,4 +85,5 @@ Files can be replaced with the :func:`replace` method. This works just like
the :func:`put` method so even metadata can (and should) be replaced::
another_marmot = open('another_marmot.png', 'rb')
marmot.photo.replace(another_marmot, content_type='image/png')
marmot.photo.replace(another_marmot, content_type='image/png') # Replaces the GridFS document
marmot.save() # Replaces the GridFS reference contained in marmot instance

View File

@ -10,7 +10,10 @@ User Guide
defining-documents
document-instances
querying
validation
gridfs
signals
text-indexes
migration
logging-monitoring
mongomock

View File

@ -12,7 +12,7 @@ MongoEngine is available on PyPI, so you can use :program:`pip`:
.. code-block:: console
$ pip install mongoengine
$ python -m pip install mongoengine
Alternatively, if you don't have setuptools installed, `download it from PyPi
<http://pypi.python.org/pypi/mongoengine/>`_ and run

View File

@ -0,0 +1,80 @@
==================
Logging/Monitoring
==================
It is possible to use `pymongo.monitoring <https://api.mongodb.com/python/current/api/pymongo/monitoring.html>`_ to monitor
the driver events (e.g: queries, connections, etc). This can be handy if you want to monitor the queries issued by
MongoEngine to the driver.
To use `pymongo.monitoring` with MongoEngine, you need to make sure that you are registering the listeners
**before** establishing the database connection (i.e calling `connect`):
The following snippet provides a basic logging of all command events:
.. code-block:: python
import logging
from pymongo import monitoring
from mongoengine import *
log = logging.getLogger()
log.setLevel(logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
class CommandLogger(monitoring.CommandListener):
def started(self, event):
log.debug("Command {0.command_name} with request id "
"{0.request_id} started on server "
"{0.connection_id}".format(event))
def succeeded(self, event):
log.debug("Command {0.command_name} with request id "
"{0.request_id} on server {0.connection_id} "
"succeeded in {0.duration_micros} "
"microseconds".format(event))
def failed(self, event):
log.debug("Command {0.command_name} with request id "
"{0.request_id} on server {0.connection_id} "
"failed in {0.duration_micros} "
"microseconds".format(event))
monitoring.register(CommandLogger())
class Jedi(Document):
name = StringField()
connect()
log.info('GO!')
log.info('Saving an item through MongoEngine...')
Jedi(name='Obi-Wan Kenobii').save()
log.info('Querying through MongoEngine...')
obiwan = Jedi.objects.first()
log.info('Updating through MongoEngine...')
obiwan.name = 'Obi-Wan Kenobi'
obiwan.save()
Executing this prints the following output::
INFO:root:GO!
INFO:root:Saving an item through MongoEngine...
DEBUG:root:Command insert with request id 1681692777 started on server ('localhost', 27017)
DEBUG:root:Command insert with request id 1681692777 on server ('localhost', 27017) succeeded in 562 microseconds
INFO:root:Querying through MongoEngine...
DEBUG:root:Command find with request id 1714636915 started on server ('localhost', 27017)
DEBUG:root:Command find with request id 1714636915 on server ('localhost', 27017) succeeded in 341 microseconds
INFO:root:Updating through MongoEngine...
DEBUG:root:Command update with request id 1957747793 started on server ('localhost', 27017)
DEBUG:root:Command update with request id 1957747793 on server ('localhost', 27017) succeeded in 455 microseconds
More details can of course be obtained by checking the `event` argument from the `CommandListener`.

308
docs/guide/migration.rst Normal file
View File

@ -0,0 +1,308 @@
===================
Documents migration
===================
The structure of your documents and their associated mongoengine schemas are likely
to change over the lifetime of an application. This section provides guidance and
recommendations on how to deal with migrations.
Due to the very flexible nature of mongodb, migrations of models aren't trivial and
for people that know about `alembic` for `sqlalchemy`, there is unfortunately no equivalent
library that will manage the migration in an automatic fashion for mongoengine.
Example 1: Addition of a field
==============================
Let's start by taking a simple example of a model change and review the different option you
have to deal with the migration.
Let's assume we start with the following schema and save an instance:
.. code-block:: python
class User(Document):
name = StringField()
User(name="John Doe").save()
# print the objects as they exist in mongodb
print(User.objects().as_pymongo()) # [{u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John Doe'}]
On the next version of your application, let's now assume that a new field `enabled` gets added to the
existing ``User`` model with a `default=True`. Thus you simply update the ``User`` class to the following:
.. code-block:: python
class User(Document):
name = StringField(required=True)
enabled = BooleanField(default=True)
Without applying any migration, we now reload an object from the database into the ``User`` class
and checks its `enabled` attribute:
.. code-block:: python
assert User.objects.count() == 1
user = User.objects().first()
assert user.enabled is True
assert User.objects(enabled=True).count() == 0 # uh?
assert User.objects(enabled=False).count() == 0 # uh?
# this is consistent with what we have in the database
# in fact, 'enabled' does not exist
print(User.objects().as_pymongo().first()) # {u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John'}
assert User.objects(enabled=None).count() == 1
As you can see, even if the document wasn't updated, mongoengine applies the default value seamlessly when it
loads the pymongo dict into a ``User`` instance. At first sight it looks like you don't need to migrate the
existing documents when adding new fields but this actually leads to inconsistencies when it comes to querying.
In fact, when querying, mongoengine isn't trying to account for the default value of the new field and so
if you don't actually migrate the existing documents, you are taking a risk that querying/updating
will be missing relevant record.
When adding fields/modifying default values, you can use any of the following to do the migration
as a standalone script:
.. code-block:: python
# Use mongoengine to set a default value for a given field
User.objects().update(enabled=True)
# or use pymongo
user_coll = User._get_collection()
user_coll.update_many({}, {'$set': {'enabled': True}})
Example 2: Inheritance change
=============================
Let's consider the following example:
.. code-block:: python
class Human(Document):
name = StringField()
meta = {"allow_inheritance": True}
class Jedi(Human):
dark_side = BooleanField()
light_saber_color = StringField()
Jedi(name="Darth Vader", dark_side=True, light_saber_color="red").save()
Jedi(name="Obi Wan Kenobi", dark_side=False, light_saber_color="blue").save()
assert Human.objects.count() == 2
assert Jedi.objects.count() == 2
# Let's check how these documents got stored in mongodb
print(Jedi.objects.as_pymongo())
# [
# {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'},
# {'_id': ObjectId('5fac4ac4f61d7fb06046e0fa'), '_cls': 'Human.Jedi', 'name': 'Obi Wan Kenobi', 'dark_side': False, 'light_saber_color': 'blue'}
# ]
As you can observe, when you use inheritance, MongoEngine stores a field named '_cls' behind the scene to keep
track of the Document class.
Let's now take the scenario that you want to refactor the inheritance schema and:
- Have the Jedi's with dark_side=True/False become GoodJedi's/DarkSith
- get rid of the 'dark_side' field
move to the following schemas:
.. code-block:: python
# unchanged
class Human(Document):
name = StringField()
meta = {"allow_inheritance": True}
# attribute 'dark_side' removed
class GoodJedi(Human):
light_saber_color = StringField()
# new class
class BadSith(Human):
light_saber_color = StringField()
MongoEngine doesn't know about the change or how to map them with the existing data
so if you don't apply any migration, you will observe a strange behavior, as if the collection was suddenly
empty.
.. code-block:: python
# As a reminder, the documents that we inserted
# have the _cls field = 'Human.Jedi'
# Following has no match
# because the query that is used behind the scene is
# filtering on {'_cls': 'Human.GoodJedi'}
assert GoodJedi.objects().count() == 0
# Following has also no match
# because it is filtering on {'_cls': {'$in': ('Human', 'Human.GoodJedi', 'Human.BadSith')}}
# which has no match
assert Human.objects.count() == 0
assert Human.objects.first() is None
# If we bypass MongoEngine and make use of underlying driver (PyMongo)
# we can see that the documents are there
humans_coll = Human._get_collection()
assert humans_coll.count_documents({}) == 2
# print first document
print(humans_coll.find_one())
# {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'}
As you can see, first obvious problem is that we need to modify '_cls' values based on existing values of
'dark_side' documents.
.. code-block:: python
humans_coll = Human._get_collection()
old_class = 'Human.Jedi'
good_jedi_class = 'Human.GoodJedi'
bad_sith_class = 'Human.BadSith'
humans_coll.update_many({'_cls': old_class, 'dark_side': False}, {'$set': {'_cls': good_jedi_class}})
humans_coll.update_many({'_cls': old_class, 'dark_side': True}, {'$set': {'_cls': bad_sith_class}})
Let's now check if querying improved in MongoEngine:
.. code-block:: python
assert GoodJedi.objects().count() == 1 # Hoorah!
assert BadSith.objects().count() == 1 # Hoorah!
assert Human.objects.count() == 2 # Hoorah!
# let's now check that documents load correctly
jedi = GoodJedi.objects().first()
# raises FieldDoesNotExist: The fields "{'dark_side'}" do not exist on the document "Human.GoodJedi"
In fact we only took care of renaming the _cls values but we havn't removed the 'dark_side' fields
which does not exist anymore on the GoodJedi's and BadSith's models.
Let's remove the field from the collections:
.. code-block:: python
humans_coll = Human._get_collection()
humans_coll.update_many({}, {'$unset': {'dark_side': 1}})
.. note:: We did this migration in 2 different steps for the sake of example but it could have been combined
with the migration of the _cls fields: ::
humans_coll.update_many(
{'_cls': old_class, 'dark_side': False},
{
'$set': {'_cls': good_jedi_class},
'$unset': {'dark_side': 1}
}
)
And verify that the documents now load correctly:
.. code-block:: python
jedi = GoodJedi.objects().first()
assert jedi.name == "Obi Wan Kenobi"
sith = BadSith.objects().first()
assert sith.name == "Darth Vader"
An other way of dealing with this migration is to iterate over
the documents and update/replace them one by one. This is way slower but
it is often useful for complex migrations of Document models.
.. code-block:: python
for doc in humans_coll.find():
if doc['_cls'] == 'Human.Jedi':
doc['_cls'] = 'Human.BadSith' if doc['dark_side'] else 'Human.GoodJedi'
doc.pop('dark_side')
humans_coll.replace_one({'_id': doc['_id']}, doc)
.. warning:: Be aware of this `flaw <https://groups.google.com/g/mongodb-user/c/AFC1ia7MHzk>`_ if you modify documents while iterating
Example 4: Index removal
========================
If you remove an index from your Document class, or remove an indexed Field from your Document class,
you'll need to manually drop the corresponding index. MongoEngine will not do that for you.
The way to deal with this case is to identify the name of the index to drop with `index_information()`, and then drop
it with `drop_index()`
Let's for instance assume that you start with the following Document class
.. code-block:: python
class User(Document):
name = StringField(index=True)
meta = {"indexes": ["name"]}
User(name="John Doe").save()
As soon as you start interacting with the Document collection (when `.save()` is called in this case),
it would create the following indexes:
.. code-block:: python
print(User._get_collection().index_information())
# {
# '_id_': {'key': [('_id', 1)], 'v': 2},
# 'name_1': {'background': False, 'key': [('name', 1)], 'v': 2},
# }
Thus: '_id' which is the default index and 'name_1' which is our custom index.
If you would remove the 'name' field or its index, you would have to call:
.. code-block:: python
User._get_collection().drop_index('name_1')
.. note:: When adding new fields or new indexes, MongoEngine will take care of creating them
(unless `auto_create_index` is disabled) ::
Recommendations
===============
- Write migration scripts whenever you do changes to the model schemas
- Using :class:`~mongoengine.DynamicDocument` or ``meta = {"strict": False}`` may help to avoid some migrations or to have the 2 versions of your application to co-exist.
- Write post-processing checks to verify that migrations script worked. See below
Post-processing checks
======================
The following recipe can be used to sanity check a Document collection after you applied migration.
It does not make any assumption on what was migrated, it will fetch 1000 objects randomly and
run some quick checks on the documents to make sure the document looks OK. As it is, it will fail
on the first occurrence of an error but this is something that can be adapted based on your needs.
.. code-block:: python
def get_random_oids(collection, sample_size):
pipeline = [{"$project": {'_id': 1}}, {"$sample": {"size": sample_size}}]
return [s['_id'] for s in collection.aggregate(pipeline)]
def get_random_documents(DocCls, sample_size):
doc_collection = DocCls._get_collection()
random_oids = get_random_oids(doc_collection, sample_size)
return DocCls.objects(id__in=random_oids)
def check_documents(DocCls, sample_size):
for doc in get_random_documents(DocCls, sample_size):
# general validation (types and values)
doc.validate()
# load all subfields,
# this may trigger additional queries if you have ReferenceFields
# so it may be slow
for field in doc._fields:
try:
getattr(doc, field)
except Exception:
LOG.warning(f"Could not load field {field} in Document {doc.id}")
raise
check_documents(Human, sample_size=1000)

View File

@ -2,10 +2,10 @@
Use mongomock for testing
==============================
`mongomock <https://github.com/vmalloc/mongomock/>`_ is a package to do just
`mongomock <https://github.com/vmalloc/mongomock/>`_ is a package to do just
what the name implies, mocking a mongo database.
To use with mongoengine, simply specify mongomock when connecting with
To use with mongoengine, simply specify mongomock when connecting with
mongoengine:
.. code-block:: python
@ -19,3 +19,30 @@ or with an alias:
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
conn = get_connection('testdb')
Example of test file:
---------------------
.. code-block:: python
import unittest
from mongoengine import connect, disconnect
class Person(Document):
name = StringField()
class TestPerson(unittest.TestCase):
@classmethod
def setUpClass(cls):
connect('mongoenginetest', host='mongomock://localhost')
@classmethod
def tearDownClass(cls):
disconnect()
def test_thing(self):
pers = Person(name='John')
pers.save()
fresh_pers = Person.objects().first()
assert fresh_pers.name == 'John'

View File

@ -64,7 +64,7 @@ Available operators are as follows:
* ``gt`` -- greater than
* ``gte`` -- greater than or equal to
* ``not`` -- negate a standard check, may be used before other operators (e.g.
``Q(age__not__mod=5)``)
``Q(age__not__mod=(5, 0))``)
* ``in`` -- value is in list (a list of values should be provided)
* ``nin`` -- value is not in list (a list of values should be provided)
* ``mod`` -- ``value % x == y``, where ``x`` and ``y`` are two provided values
@ -86,6 +86,10 @@ expressions:
* ``istartswith`` -- string field starts with value (case insensitive)
* ``endswith`` -- string field ends with value
* ``iendswith`` -- string field ends with value (case insensitive)
* ``wholeword`` -- string field contains whole word
* ``iwholeword`` -- string field contains whole word (case insensitive)
* ``regex`` -- string field match by regex
* ``iregex`` -- string field match by regex (case insensitive)
* ``match`` -- performs an $elemMatch so you can match an entire document within an array
@ -222,12 +226,24 @@ keyword argument::
.. versionadded:: 0.4
Sorting/Ordering results
========================
It is possible to order the results by 1 or more keys using :meth:`~mongoengine.queryset.QuerySet.order_by`.
The order may be specified by prepending each of the keys by "+" or "-". Ascending order is assumed if there's no prefix.::
# Order by ascending date
blogs = BlogPost.objects().order_by('date') # equivalent to .order_by('+date')
# Order by ascending date first, then descending title
blogs = BlogPost.objects().order_by('+date', '-title')
Limiting and skipping results
=============================
Just as with traditional ORMs, you may limit the number of results returned or
skip a number or results in you query.
:meth:`~mongoengine.queryset.QuerySet.limit` and
:meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on
:meth:`~mongoengine.queryset.QuerySet.skip` methods are available on
:class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax
is preferred for achieving this::
@ -349,9 +365,9 @@ Just as with limiting and skipping results, there is a method on a
You could technically use ``len(User.objects)`` to get the same result, but it
would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`.
When you execute a server-side count query, you let MongoDB do the heavy
lifting and you receive a single integer over the wire. Meanwhile, len()
lifting and you receive a single integer over the wire. Meanwhile, ``len()``
retrieves all the results, places them in a local cache, and finally counts
them. If we compare the performance of the two operations, len() is much slower
them. If we compare the performance of the two operations, ``len()`` is much slower
than :meth:`~mongoengine.queryset.QuerySet.count`.
Further aggregation
@ -386,6 +402,25 @@ would be generating "tag-clouds"::
top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10]
MongoDB aggregation API
-----------------------
If you need to run aggregation pipelines, MongoEngine provides an entry point to `Pymongo's aggregation framework <https://api.mongodb.com/python/current/examples/aggregation.html#aggregation-framework>`_
through :meth:`~mongoengine.queryset.QuerySet.aggregate`. Check out Pymongo's documentation for the syntax and pipeline.
An example of its use would be::
class Person(Document):
name = StringField()
Person(name='John').save()
Person(name='Bob').save()
pipeline = [
{"$sort" : {"name" : -1}},
{"$project": {"_id": 0, "name": {"$toUpper": "$name"}}}
]
data = Person.objects().aggregate(pipeline)
assert data == [{'name': 'BOB'}, {'name': 'JOHN'}]
Query efficiency and performance
================================
@ -456,14 +491,14 @@ data. To turn off dereferencing of the results of a query use
:func:`~mongoengine.queryset.QuerySet.no_dereference` on the queryset like so::
post = Post.objects.no_dereference().first()
assert(isinstance(post.author, ObjectId))
assert(isinstance(post.author, DBRef))
You can also turn off all dereferencing for a fixed period by using the
:class:`~mongoengine.context_managers.no_dereference` context manager::
with no_dereference(Post) as Post:
post = Post.objects.first()
assert(isinstance(post.author, ObjectId))
assert(isinstance(post.author, DBRef))
# Outside the context manager dereferencing occurs.
assert(isinstance(post.author, User))
@ -512,7 +547,10 @@ Documents may be updated atomically by using the
There are several different "modifiers" that you may use with these methods:
* ``set`` -- set a particular value
* ``set_on_insert`` -- set only if this is new document `need to add upsert=True`_
* ``unset`` -- delete a particular value (since MongoDB v1.3)
* ``max`` -- update only if value is bigger
* ``min`` -- update only if value is smaller
* ``inc`` -- increment a value by a given amount
* ``dec`` -- decrement a value by a given amount
* ``push`` -- append a value to a list
@ -521,6 +559,7 @@ There are several different "modifiers" that you may use with these methods:
* ``pull`` -- remove a value from a list
* ``pull_all`` -- remove several values from a list
* ``add_to_set`` -- add value to a list only if its not in the list already
* ``rename`` -- rename the key name
.. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/
@ -565,10 +604,20 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`::
>>> post.tags
['database', 'mongodb']
From MongoDB version 2.6, push operator supports $position value which allows
to push values with index::
>>> post = BlogPost(title="Test", tags=["mongo"])
>>> post.save()
>>> post.update(push__tags__0=["database", "code"])
>>> post.reload()
>>> post.tags
['database', 'code', 'mongo']
.. note::
Currently only top level lists are handled, future versions of mongodb /
pymongo plan to support nested positional operators. See `The $ positional
operator <http://www.mongodb.org/display/DOCS/Updating#Updating-The%24positionaloperator>`_.
operator <https://docs.mongodb.com/manual/tutorial/update-documents/#Updating-The%24positionaloperator>`_.
Server-side javascript execution
================================

View File

@ -43,10 +43,10 @@ Available signals include:
has taken place but before saving.
`post_save`
Called within :meth:`~mongoengine.Document.save` after all actions
(validation, insert/update, cascades, clearing dirty flags) have completed
successfully. Passed the additional boolean keyword argument `created` to
indicate if the save was an insert or an update.
Called within :meth:`~mongoengine.Document.save` after most actions
(validation, insert/update, and cascades, but not clearing dirty flags) have
completed successfully. Passed the additional boolean keyword argument
`created` to indicate if the save was an insert or an update.
`pre_delete`
Called within :meth:`~mongoengine.Document.delete` prior to
@ -113,6 +113,10 @@ handlers within your subclass::
signals.pre_save.connect(Author.pre_save, sender=Author)
signals.post_save.connect(Author.post_save, sender=Author)
.. warning::
Note that EmbeddedDocument only supports pre/post_init signals. pre/post_save, etc should be attached to Document's class only. Attaching pre_save to an EmbeddedDocument is ignored silently.
Finally, you can also use this small decorator to quickly create a number of
signals and attach them to your :class:`~mongoengine.Document` or
:class:`~mongoengine.EmbeddedDocument` subclasses as class decorators::

View File

@ -8,7 +8,7 @@ After MongoDB 2.4 version, supports search documents by text indexes.
Defining a Document with text index
===================================
Use the *$* prefix to set a text index, Look the declaration::
class News(Document):
title = StringField()
content = StringField()
@ -35,10 +35,10 @@ Saving a document::
content="Various improvements").save()
Next, start a text search using :attr:`QuerySet.search_text` method::
document = News.objects.search_text('testing').first()
document.title # may be: "Using mongodb text search"
document = News.objects.search_text('released').first()
document.title # may be: "MongoEngine 0.9 released"
@ -48,4 +48,4 @@ Ordering by text score
::
objects = News.objects.search('mongo').order_by('$text_score')
objects = News.objects.search_text('mongo').order_by('$text_score')

122
docs/guide/validation.rst Normal file
View File

@ -0,0 +1,122 @@
====================
Document Validation
====================
By design, MongoEngine strictly validates the documents right before they are inserted in MongoDB
and makes sure they are consistent with the fields defined in your models.
MongoEngine makes the assumption that the documents that exists in the DB are compliant with the schema.
This means that Mongoengine will not validate a document when an object is loaded from the DB into an instance
of your model but this operation may fail under some circumstances (e.g. if there is a field in
the document fetched from the database that is not defined in your model).
Built-in validation
===================
Mongoengine provides different fields that encapsulate the corresponding validation
out of the box. Validation runs when calling `.validate()` or `.save()`
.. code-block:: python
from mongoengine import Document, EmailField
class User(Document):
email = EmailField()
age = IntField(min_value=0, max_value=99)
user = User(email='invalid@', age=24)
user.validate() # raises ValidationError (Invalid email address: ['email'])
user.save() # raises ValidationError (Invalid email address: ['email'])
user2 = User(email='john.doe@garbage.com', age=1000)
user2.save() # raises ValidationError (Integer value is too large: ['age'])
Custom validation
=================
The following feature can be used to customize the validation:
* Field `validation` parameter
.. code-block:: python
def not_john_doe(name):
if name == 'John Doe':
raise ValidationError("John Doe is not a valid name")
class Person(Document):
full_name = StringField(validation=not_john_doe)
Person(full_name='Billy Doe').save()
Person(full_name='John Doe').save() # raises ValidationError (John Doe is not a valid name)
* Document `clean` method
This method is called as part of :meth:`~mongoengine.document.Document.save` and should be used to provide
custom model validation and/or to modify some of the field values prior to validation.
For instance, you could use it to automatically provide a value for a field, or to do validation
that requires access to more than a single field.
.. code-block:: python
class Essay(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
pub_date = DateTimeField()
def clean(self):
# Validate that only published essays have a `pub_date`
if self.status == 'Draft' and self.pub_date is not None:
raise ValidationError('Draft entries should not have a publication date.')
# Set the pub_date for published items if not set.
if self.status == 'Published' and self.pub_date is None:
self.pub_date = datetime.now()
.. note::
Cleaning is only called if validation is turned on and when calling
:meth:`~mongoengine.Document.save`.
* Adding custom Field classes
We recommend as much as possible to use fields provided by MongoEngine. However, it is also possible
to subclass a Field and encapsulate some validation by overriding the `validate` method
.. code-block:: python
class AgeField(IntField):
def validate(self, value):
super(AgeField, self).validate(value) # let IntField.validate run first
if value == 60:
self.error('60 is not allowed')
class Person(Document):
age = AgeField(min_value=0, max_value=99)
Person(age=20).save() # passes
Person(age=1000).save() # raises ValidationError (Integer value is too large: ['age'])
Person(age=60).save() # raises ValidationError (Person:None) (60 is not allowed: ['age'])
.. note::
When overriding `validate`, use `self.error("your-custom-error")` instead of raising ValidationError explicitly,
it will provide a better context with the error message
Skipping validation
====================
Although discouraged as it allows to violate fields constraints, if for some reason you need to disable
the validation and cleaning of a document when you call :meth:`~mongoengine.document.Document.save`, you can use `.save(validate=False)`.
.. code-block:: python
class Person(Document):
age = IntField(max_value=100)
Person(age=1000).save() # raises ValidationError (Integer value is too large)
Person(age=1000).save(validate=False)
person = Person.objects.first()
assert person.age == 1000

View File

@ -7,7 +7,7 @@ MongoDB. To install it, simply run
.. code-block:: console
$ pip install -U mongoengine
$ python -m pip install -U mongoengine
:doc:`tutorial`
A quick tutorial building a tumblelog to get you up and running with
@ -23,9 +23,18 @@ MongoDB. To install it, simply run
:doc:`upgrade`
How to upgrade MongoEngine.
:doc:`faq`
Frequently Asked Questions
:doc:`django`
Using MongoEngine and Django
MongoDB and driver support
--------------------------
MongoEngine is based on the PyMongo driver and tested against multiple versions of MongoDB.
For further details, please refer to the `readme <https://github.com/MongoEngine/mongoengine#mongoengine>`_.
Community
---------
@ -73,6 +82,7 @@ formats for offline reading.
apireference
changelog
upgrade
faq
django
Indices and tables
@ -81,4 +91,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

3
docs/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Sphinx==3.3.0
sphinx-rtd-theme==0.5.0
readthedocs-sphinx-ext==2.1.1

View File

@ -18,7 +18,7 @@ location --- running it locally will be easier, but if that is not an option
then it may be run on a remote server. If you haven't installed MongoEngine,
simply use pip to install it like so::
$ pip install mongoengine
$ python -m pip install mongoengine
Before we can start using MongoEngine, we need to tell it how to connect to our
instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect`
@ -86,7 +86,7 @@ of them stand out as particularly intuitive solutions.
Posts
^^^^^
Happily mongoDB *isn't* a relational database, so we're not going to do it that
Happily MongoDB *isn't* a relational database, so we're not going to do it that
way. As it turns out, we can use MongoDB's schemaless nature to provide us with
a much nicer solution. We will store all of the posts in *one collection* and
each post type will only store the fields it needs. If we later want to add
@ -153,7 +153,7 @@ post. This works, but there is no real reason to be storing the comments
separately from their associated posts, other than to work around the
relational model. Using MongoDB we can store the comments as a list of
*embedded documents* directly on a post document. An embedded document should
be treated no differently that a regular document; it just doesn't have its own
be treated no differently than a regular document; it just doesn't have its own
collection in the database. Using MongoEngine, we can define the structure of
embedded documents, along with utility methods, in exactly the same way we do
with regular documents::

View File

@ -6,6 +6,23 @@ Development
***********
(Fill this out whenever you introduce breaking changes to MongoEngine)
URLField's constructor no longer takes `verify_exists`
0.15.0
******
0.14.0
******
This release includes a few bug fixes and a significant code cleanup. The most
important change is that `QuerySet.as_pymongo` no longer supports a
`coerce_types` mode. If you used it in the past, a) please let us know of your
use case, b) you'll need to override `as_pymongo` to get the desired outcome.
This release also makes the EmbeddedDocument not hashable by default. If you
use embedded documents in sets or dictionaries, you might have to override
`__hash__` and implement a hashing logic specific to your use case. See #1528
for the reason behind this change.
0.13.0
******
This release adds Unicode support to the `EmailField` and changes its
@ -35,7 +52,7 @@ rename its occurrences.
This release includes a major rehaul of MongoEngine's code quality and
introduces a few breaking changes. It also touches many different parts of
the package and although all the changes have been tested and scrutinized,
you're encouraged to thorougly test the upgrade.
you're encouraged to thoroughly test the upgrade.
First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`.
If you import or catch this exception, you'll need to rename it in your code.
@ -68,10 +85,10 @@ by default from now on.
The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: ::
pip uninstall pymongo
pip uninstall mongoengine
pip install pymongo==2.8
pip install mongoengine
python -m pip uninstall pymongo
python -m pip uninstall mongoengine
python -m pip install pymongo==2.8
python -m pip install mongoengine
0.8.7
*****
@ -136,7 +153,7 @@ inherited classes like so: ::
# 4. Remove indexes
info = collection.index_information()
indexes_to_drop = [key for key, value in info.iteritems()
indexes_to_drop = [key for key, value in info.items()
if '_types' in dict(value['key'])]
for index in indexes_to_drop:
collection.drop_index(index)

View File

@ -1,36 +1,43 @@
# Import submodules so that we can expose their __all__
from mongoengine import connection
from mongoengine import document
from mongoengine import errors
from mongoengine import fields
from mongoengine import queryset
from mongoengine import signals
from mongoengine import (
connection,
document,
errors,
fields,
queryset,
signals,
)
# Import everything from each submodule so that it can be accessed via
# mongoengine, e.g. instead of `from mongoengine.connection import connect`,
# users can simply use `from mongoengine import connect`, or even
# `from mongoengine import *` and then `connect('testdb')`.
from mongoengine.connection import *
from mongoengine.document import *
from mongoengine.errors import *
from mongoengine.fields import *
from mongoengine.queryset import *
from mongoengine.signals import *
from mongoengine.connection import * # noqa: F401
from mongoengine.document import * # noqa: F401
from mongoengine.errors import * # noqa: F401
from mongoengine.fields import * # noqa: F401
from mongoengine.queryset import * # noqa: F401
from mongoengine.signals import * # noqa: F401
__all__ = (
list(document.__all__)
+ list(fields.__all__)
+ list(connection.__all__)
+ list(queryset.__all__)
+ list(signals.__all__)
+ list(errors.__all__)
)
__all__ = (list(document.__all__) + list(fields.__all__) +
list(connection.__all__) + list(queryset.__all__) +
list(signals.__all__) + list(errors.__all__))
VERSION = (0, 13, 0)
VERSION = (0, 23, 1)
def get_version():
"""Return the VERSION as a string, e.g. for VERSION == (0, 10, 7),
return '0.10.7'.
"""Return the VERSION as a string.
For example, if `VERSION == (0, 10, 7)`, return '0.10.7'.
"""
return '.'.join(map(str, VERSION))
return ".".join(map(str, VERSION))
__version__ = get_version()

View File

@ -12,17 +12,22 @@ from mongoengine.base.metaclasses import *
__all__ = (
# common
'UPDATE_OPERATORS', '_document_registry', 'get_document',
"UPDATE_OPERATORS",
"_document_registry",
"get_document",
# datastructures
'BaseDict', 'BaseList', 'EmbeddedDocumentList',
"BaseDict",
"BaseList",
"EmbeddedDocumentList",
"LazyReference",
# document
'BaseDocument',
"BaseDocument",
# fields
'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
"BaseField",
"ComplexBaseField",
"ObjectIdField",
"GeoJsonBaseField",
# metaclasses
'DocumentMetaclass', 'TopLevelDocumentMetaclass'
"DocumentMetaclass",
"TopLevelDocumentMetaclass",
)

View File

@ -1,31 +1,62 @@
from mongoengine.errors import NotRegistered
__all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry')
__all__ = ("UPDATE_OPERATORS", "get_document", "_document_registry")
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
'push_all', 'pull', 'pull_all', 'add_to_set',
'set_on_insert', 'min', 'max', 'rename'])
UPDATE_OPERATORS = {
"set",
"unset",
"inc",
"dec",
"mul",
"pop",
"push",
"push_all",
"pull",
"pull_all",
"add_to_set",
"set_on_insert",
"min",
"max",
"rename",
}
_document_registry = {}
def get_document(name):
"""Get a document class by name."""
"""Get a registered Document class by name."""
doc = _document_registry.get(name, None)
if not doc:
# Possible old style name
single_end = name.split('.')[-1]
compound_end = '.%s' % single_end
possible_match = [k for k in _document_registry.keys()
if k.endswith(compound_end) or k == single_end]
single_end = name.split(".")[-1]
compound_end = ".%s" % single_end
possible_match = [
k for k in _document_registry if k.endswith(compound_end) or k == single_end
]
if len(possible_match) == 1:
doc = _document_registry.get(possible_match.pop(), None)
if not doc:
raise NotRegistered("""
raise NotRegistered(
"""
`%s` has not been registered in the document registry.
Importing the document class automatically registers it, has it
been imported?
""".strip() % name)
""".strip()
% name
)
return doc
def _get_documents_by_db(connection_alias, default_connection_alias):
"""Get all registered Documents class attached to a given database"""
def get_doc_alias(doc_cls):
return doc_cls._meta.get("db_alias", default_connection_alias)
return [
doc_cls
for doc_cls in _document_registry.values()
if get_doc_alias(doc_cls) == connection_alias
]

View File

@ -1,12 +1,41 @@
import itertools
import weakref
import six
from bson import DBRef
from mongoengine.common import _import_class
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList')
__all__ = (
"BaseDict",
"StrictDict",
"BaseList",
"EmbeddedDocumentList",
"LazyReference",
)
def mark_as_changed_wrapper(parent_method):
"""Decorator that ensures _mark_as_changed method gets called."""
def wrapper(self, *args, **kwargs):
# Can't use super() in the decorator.
result = parent_method(self, *args, **kwargs)
self._mark_as_changed()
return result
return wrapper
def mark_key_as_changed_wrapper(parent_method):
"""Decorator that ensures _mark_as_changed method gets called with the key argument"""
def wrapper(self, key, *args, **kwargs):
# Can't use super() in the decorator.
result = parent_method(self, key, *args, **kwargs)
self._mark_as_changed(key)
return result
return wrapper
class BaseDict(dict):
@ -17,46 +46,36 @@ class BaseDict(dict):
_name = None
def __init__(self, dict_items, instance, name):
Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument')
BaseDocument = _import_class("BaseDocument")
if isinstance(instance, (Document, EmbeddedDocument)):
if isinstance(instance, BaseDocument):
self._instance = weakref.proxy(instance)
self._name = name
super(BaseDict, self).__init__(dict_items)
super().__init__(dict_items)
def __getitem__(self, key, *args, **kwargs):
value = super(BaseDict, self).__getitem__(key)
def get(self, key, default=None):
# get does not use __getitem__ by default so we must override it as well
try:
return self.__getitem__(key)
except KeyError:
return default
EmbeddedDocument = _import_class('EmbeddedDocument')
def __getitem__(self, key):
value = super().__getitem__(key)
EmbeddedDocument = _import_class("EmbeddedDocument")
if isinstance(value, EmbeddedDocument) and value._instance is None:
value._instance = self._instance
elif not isinstance(value, BaseDict) and isinstance(value, dict):
value = BaseDict(value, None, '%s.%s' % (self._name, key))
super(BaseDict, self).__setitem__(key, value)
elif isinstance(value, dict) and not isinstance(value, BaseDict):
value = BaseDict(value, None, f"{self._name}.{key}")
super().__setitem__(key, value)
value._instance = self._instance
elif not isinstance(value, BaseList) and isinstance(value, list):
value = BaseList(value, None, '%s.%s' % (self._name, key))
super(BaseDict, self).__setitem__(key, value)
elif isinstance(value, list) and not isinstance(value, BaseList):
value = BaseList(value, None, f"{self._name}.{key}")
super().__setitem__(key, value)
value._instance = self._instance
return value
def __setitem__(self, key, value, *args, **kwargs):
self._mark_as_changed(key)
return super(BaseDict, self).__setitem__(key, value)
def __delete__(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).__delete__(*args, **kwargs)
def __delitem__(self, key, *args, **kwargs):
self._mark_as_changed(key)
return super(BaseDict, self).__delitem__(key)
def __delattr__(self, key, *args, **kwargs):
self._mark_as_changed(key)
return super(BaseDict, self).__delattr__(key)
def __getstate__(self):
self.instance = None
self._dereferenced = False
@ -66,30 +85,19 @@ class BaseDict(dict):
self = state
return self
def clear(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).clear()
def pop(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).pop(*args, **kwargs)
def popitem(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).popitem()
def setdefault(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).setdefault(*args, **kwargs)
def update(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseDict, self).update(*args, **kwargs)
__setitem__ = mark_key_as_changed_wrapper(dict.__setitem__)
__delattr__ = mark_key_as_changed_wrapper(dict.__delattr__)
__delitem__ = mark_key_as_changed_wrapper(dict.__delitem__)
pop = mark_as_changed_wrapper(dict.pop)
clear = mark_as_changed_wrapper(dict.clear)
update = mark_as_changed_wrapper(dict.update)
popitem = mark_as_changed_wrapper(dict.popitem)
setdefault = mark_as_changed_wrapper(dict.setdefault)
def _mark_as_changed(self, key=None):
if hasattr(self._instance, '_mark_as_changed'):
if hasattr(self._instance, "_mark_as_changed"):
if key:
self._instance._mark_as_changed('%s.%s' % (self._name, key))
self._instance._mark_as_changed(f"{self._name}.{key}")
else:
self._instance._mark_as_changed(self._name)
@ -102,52 +110,41 @@ class BaseList(list):
_name = None
def __init__(self, list_items, instance, name):
Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument')
BaseDocument = _import_class("BaseDocument")
if isinstance(instance, (Document, EmbeddedDocument)):
if isinstance(instance, BaseDocument):
self._instance = weakref.proxy(instance)
self._name = name
super(BaseList, self).__init__(list_items)
super().__init__(list_items)
def __getitem__(self, key, *args, **kwargs):
value = super(BaseList, self).__getitem__(key)
def __getitem__(self, key):
# change index to positive value because MongoDB does not support negative one
if isinstance(key, int) and key < 0:
key = len(self) + key
value = super().__getitem__(key)
EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(key, slice):
# When receiving a slice operator, we don't convert the structure and bind
# to parent's instance. This is buggy for now but would require more work to be handled properly
return value
EmbeddedDocument = _import_class("EmbeddedDocument")
if isinstance(value, EmbeddedDocument) and value._instance is None:
value._instance = self._instance
elif not isinstance(value, BaseDict) and isinstance(value, dict):
value = BaseDict(value, None, '%s.%s' % (self._name, key))
super(BaseList, self).__setitem__(key, value)
elif isinstance(value, dict) and not isinstance(value, BaseDict):
# Replace dict by BaseDict
value = BaseDict(value, None, f"{self._name}.{key}")
super().__setitem__(key, value)
value._instance = self._instance
elif not isinstance(value, BaseList) and isinstance(value, list):
value = BaseList(value, None, '%s.%s' % (self._name, key))
super(BaseList, self).__setitem__(key, value)
elif isinstance(value, list) and not isinstance(value, BaseList):
# Replace list by BaseList
value = BaseList(value, None, f"{self._name}.{key}")
super().__setitem__(key, value)
value._instance = self._instance
return value
def __iter__(self):
for i in xrange(self.__len__()):
yield self[i]
def __setitem__(self, key, value, *args, **kwargs):
if isinstance(key, slice):
self._mark_as_changed()
else:
self._mark_as_changed(key)
return super(BaseList, self).__setitem__(key, value)
def __delitem__(self, key, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).__delitem__(key)
def __setslice__(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).__setslice__(*args, **kwargs)
def __delslice__(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).__delslice__(*args, **kwargs)
yield from super().__iter__()
def __getstate__(self):
self.instance = None
@ -158,53 +155,40 @@ class BaseList(list):
self = state
return self
def __iadd__(self, other):
self._mark_as_changed()
return super(BaseList, self).__iadd__(other)
def __setitem__(self, key, value):
changed_key = key
if isinstance(key, slice):
# In case of slice, we don't bother to identify the exact elements being updated
# instead, we simply marks the whole list as changed
changed_key = None
def __imul__(self, other):
self._mark_as_changed()
return super(BaseList, self).__imul__(other)
result = super().__setitem__(key, value)
self._mark_as_changed(changed_key)
return result
def append(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).append(*args, **kwargs)
def extend(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).extend(*args, **kwargs)
def insert(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).insert(*args, **kwargs)
def pop(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).pop(*args, **kwargs)
def remove(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).remove(*args, **kwargs)
def reverse(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).reverse()
def sort(self, *args, **kwargs):
self._mark_as_changed()
return super(BaseList, self).sort(*args, **kwargs)
append = mark_as_changed_wrapper(list.append)
extend = mark_as_changed_wrapper(list.extend)
insert = mark_as_changed_wrapper(list.insert)
pop = mark_as_changed_wrapper(list.pop)
remove = mark_as_changed_wrapper(list.remove)
reverse = mark_as_changed_wrapper(list.reverse)
sort = mark_as_changed_wrapper(list.sort)
__delitem__ = mark_as_changed_wrapper(list.__delitem__)
__iadd__ = mark_as_changed_wrapper(list.__iadd__)
__imul__ = mark_as_changed_wrapper(list.__imul__)
def _mark_as_changed(self, key=None):
if hasattr(self._instance, '_mark_as_changed'):
if key:
self._instance._mark_as_changed(
'%s.%s' % (self._name, key % len(self))
)
if hasattr(self._instance, "_mark_as_changed"):
if key is not None:
self._instance._mark_as_changed(f"{self._name}.{key % len(self)}")
else:
self._instance._mark_as_changed(self._name)
class EmbeddedDocumentList(BaseList):
def __init__(self, list_items, instance, name):
super().__init__(list_items, instance, name)
self._instance = instance
@classmethod
def __match_all(cls, embedded_doc, kwargs):
@ -213,7 +197,7 @@ class EmbeddedDocumentList(BaseList):
"""
for key, expected_value in kwargs.items():
doc_val = getattr(embedded_doc, key)
if doc_val != expected_value and six.text_type(doc_val) != expected_value:
if doc_val != expected_value and str(doc_val) != expected_value:
return False
return True
@ -224,15 +208,14 @@ class EmbeddedDocumentList(BaseList):
return embedded_docs
return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)]
def __init__(self, list_items, instance, name):
super(EmbeddedDocumentList, self).__init__(list_items, instance, name)
self._instance = instance
def filter(self, **kwargs):
"""
Filters the list by only including embedded documents with the
given keyword arguments.
This method only supports simple comparison (e.g. .filter(name='John Doe'))
and does not support operators like __gte, __lte, __icontains like queryset.filter does
:param kwargs: The keyword arguments corresponding to the fields to
filter on. *Multiple arguments are treated as if they are ANDed
together.*
@ -287,12 +270,10 @@ class EmbeddedDocumentList(BaseList):
"""
values = self.__only_matches(self, kwargs)
if len(values) == 0:
raise DoesNotExist(
'%s matching query does not exist.' % self._name
)
raise DoesNotExist("%s matching query does not exist." % self._name)
elif len(values) > 1:
raise MultipleObjectsReturned(
'%d items returned, instead of 1' % len(values)
"%d items returned, instead of 1" % len(values)
)
return values[0]
@ -306,11 +287,11 @@ class EmbeddedDocumentList(BaseList):
def create(self, **values):
"""
Creates a new embedded document and saves it to the database.
Creates a new instance of the EmbeddedDocument and appends it to this EmbeddedDocumentList.
.. note::
The embedded document changes are not automatically saved
to the database after calling this method.
the instance of the EmbeddedDocument is not automatically saved to the database.
You still need to call .save() on the parent Document.
:param values: A dictionary of values for the embedded document.
:return: The new embedded document instance.
@ -350,7 +331,8 @@ class EmbeddedDocumentList(BaseList):
def update(self, **update):
"""
Updates the embedded documents with the given update values.
Updates the embedded documents with the given replacement values. This
function does not support mongoDB update operators such as ``inc__``.
.. note::
The embedded document changes are not automatically saved
@ -370,24 +352,24 @@ class EmbeddedDocumentList(BaseList):
return len(values)
class StrictDict(object):
class StrictDict:
__slots__ = ()
_special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create'])
_special_fields = {"get", "pop", "iteritems", "items", "keys", "create"}
_classes = {}
def __init__(self, **kwargs):
for k, v in kwargs.iteritems():
for k, v in kwargs.items():
setattr(self, k, v)
def __getitem__(self, key):
key = '_reserved_' + key if key in self._special_fields else key
key = "_reserved_" + key if key in self._special_fields else key
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key)
def __setitem__(self, key, value):
key = '_reserved_' + key if key in self._special_fields else key
key = "_reserved_" + key if key in self._special_fields else key
return setattr(self, key, value)
def __contains__(self, key):
@ -424,63 +406,68 @@ class StrictDict(object):
return (key for key in self.__slots__ if hasattr(self, key))
def __len__(self):
return len(list(self.iteritems()))
return len(list(self.items()))
def __eq__(self, other):
return self.items() == other.items()
return list(self.items()) == list(other.items())
def __ne__(self, other):
return self.items() != other.items()
return not (self == other)
@classmethod
def create(cls, allowed_keys):
allowed_keys_tuple = tuple(('_reserved_' + k if k in cls._special_fields else k) for k in allowed_keys)
allowed_keys_tuple = tuple(
("_reserved_" + k if k in cls._special_fields else k) for k in allowed_keys
)
allowed_keys = frozenset(allowed_keys_tuple)
if allowed_keys not in cls._classes:
class SpecificStrictDict(cls):
__slots__ = allowed_keys_tuple
def __repr__(self):
return '{%s}' % ', '.join('"{0!s}": {1!r}'.format(k, v) for k, v in self.items())
return "{%s}" % ", ".join(
f'"{k!s}": {v!r}' for k, v in self.items()
)
cls._classes[allowed_keys] = SpecificStrictDict
return cls._classes[allowed_keys]
class SemiStrictDict(StrictDict):
__slots__ = ('_extras', )
_classes = {}
class LazyReference(DBRef):
__slots__ = ("_cached_doc", "passthrough", "document_type")
def __getattr__(self, attr):
try:
super(SemiStrictDict, self).__getattr__(attr)
except AttributeError:
try:
return self.__getattribute__('_extras')[attr]
except KeyError as e:
raise AttributeError(e)
def fetch(self, force=False):
if not self._cached_doc or force:
self._cached_doc = self.document_type.objects.get(pk=self.pk)
if not self._cached_doc:
raise DoesNotExist("Trying to dereference unknown document %s" % (self))
return self._cached_doc
def __setattr__(self, attr, value):
try:
super(SemiStrictDict, self).__setattr__(attr, value)
except AttributeError:
try:
self._extras[attr] = value
except AttributeError:
self._extras = {attr: value}
@property
def pk(self):
return self.id
def __delattr__(self, attr):
try:
super(SemiStrictDict, self).__delattr__(attr)
except AttributeError:
try:
del self._extras[attr]
except KeyError as e:
raise AttributeError(e)
def __init__(self, document_type, pk, cached_doc=None, passthrough=False):
self.document_type = document_type
self._cached_doc = cached_doc
self.passthrough = passthrough
super().__init__(self.document_type._get_collection_name(), pk)
def __iter__(self):
def __getitem__(self, name):
if not self.passthrough:
raise KeyError()
document = self.fetch()
return document[name]
def __getattr__(self, name):
if not object.__getattribute__(self, "passthrough"):
raise AttributeError()
document = self.fetch()
try:
extras_iter = iter(self.__getattribute__('_extras'))
except AttributeError:
extras_iter = ()
return itertools.chain(super(SemiStrictDict, self).__iter__(), extras_iter)
return document[name]
except KeyError:
raise AttributeError()
def __repr__(self):
return f"<LazyReference({self.document_type}, {self.pk!r})>"

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,27 @@
import operator
import warnings
import weakref
from bson import DBRef, ObjectId, SON
import pymongo
import six
from bson import SON, DBRef, ObjectId
from mongoengine.base.common import UPDATE_OPERATORS
from mongoengine.base.datastructures import (BaseDict, BaseList,
EmbeddedDocumentList)
from mongoengine.base.datastructures import (
BaseDict,
BaseList,
EmbeddedDocumentList,
)
from mongoengine.common import _import_class
from mongoengine.errors import ValidationError
from mongoengine.errors import DeprecatedError, ValidationError
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField")
__all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
'GeoJsonBaseField')
class BaseField(object):
class BaseField:
"""A base class for fields in a MongoDB document. Instances of this class
may be added to subclasses of `Document` to define a document's schema.
.. versionchanged:: 0.5 - added verbose and help text
"""
name = None
name = None # set in TopLevelDocumentMetaclass
_geo_index = False
_auto_gen = False # Call `generate` to generate a value
_auto_dereference = True
@ -34,14 +32,23 @@ class BaseField(object):
creation_counter = 0
auto_creation_counter = -1
def __init__(self, db_field=None, name=None, required=False, default=None,
unique=False, unique_with=None, primary_key=False,
validation=None, choices=None, null=False, sparse=False,
**kwargs):
def __init__(
self,
db_field=None,
required=False,
default=None,
unique=False,
unique_with=None,
primary_key=False,
validation=None,
choices=None,
null=False,
sparse=False,
**kwargs,
):
"""
:param db_field: The database field to store this field in
(defaults to the name of the field)
:param name: Deprecated - use db_field
:param required: If the field is required. Whether it has to have a
value or not. Defaults to False.
:param default: (optional) The default value for this field if no value
@ -52,10 +59,10 @@ class BaseField(object):
unique with.
:param primary_key: Mark this field as the primary key. Defaults to False.
:param validation: (optional) A callable to validate the value of the
field. Generally this is deprecated in favour of the
`FIELD.validate` method
field. The callable takes the value as parameter and should raise
a ValidationError if validation fails
:param choices: (optional) The valid choices
:param null: (optional) Is the field value can be null. If no and there is a default value
:param null: (optional) If the field value can be null. If no and there is a default value
then the default value is set
:param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
means that uniqueness won't be enforced for `None` values
@ -65,11 +72,8 @@ class BaseField(object):
existing attributes. Common metadata includes `verbose_name` and
`help_text`.
"""
self.db_field = (db_field or name) if not primary_key else '_id'
self.db_field = db_field if not primary_key else "_id"
if name:
msg = 'Field\'s "name" attribute deprecated in favour of "db_field"'
warnings.warn(msg, DeprecationWarning)
self.required = required or primary_key
self.default = default
self.unique = bool(unique or unique_with)
@ -81,11 +85,15 @@ class BaseField(object):
self.sparse = sparse
self._owner_document = None
# Validate the db_field
if isinstance(self.db_field, six.string_types) and (
'.' in self.db_field or
'\0' in self.db_field or
self.db_field.startswith('$')
# Make sure db_field is a string (if it's explicitly defined).
if self.db_field is not None and not isinstance(self.db_field, str):
raise TypeError("db_field should be a string.")
# Make sure db_field doesn't contain any forbidden characters.
if isinstance(self.db_field, str) and (
"." in self.db_field
or "\0" in self.db_field
or self.db_field.startswith("$")
):
raise ValueError(
'field names cannot contain dots (".") or null characters '
@ -95,15 +103,17 @@ class BaseField(object):
# Detect and report conflicts between metadata and base properties.
conflicts = set(dir(self)) & set(kwargs)
if conflicts:
raise TypeError('%s already has attribute(s): %s' % (
self.__class__.__name__, ', '.join(conflicts)))
raise TypeError(
"%s already has attribute(s): %s"
% (self.__class__.__name__, ", ".join(conflicts))
)
# Assign metadata to the instance
# This efficient method is available because no __slots__ are defined.
self.__dict__.update(kwargs)
# Adjust the appropriate creation counter, and save our local copy.
if self.db_field == '_id':
if self.db_field == "_id":
self.creation_counter = BaseField.auto_creation_counter
BaseField.auto_creation_counter -= 1
else:
@ -111,8 +121,7 @@ class BaseField(object):
BaseField.creation_counter += 1
def __get__(self, instance, owner):
"""Descriptor for retrieving a value from a field in a document.
"""
"""Descriptor for retrieving a value from a field in a document."""
if instance is None:
# Document class being used rather than a document object
return self
@ -121,11 +130,9 @@ class BaseField(object):
return instance._data.get(self.name)
def __set__(self, instance, value):
"""Descriptor for assigning a value to a field in a document.
"""
# If setting to None and there is a default
# Then set the value to the default value
"""Descriptor for assigning a value to a field in a document."""
# If setting to None and there is a default value provided for this
# field, then set the value to the default value.
if value is None:
if self.null:
value = None
@ -136,24 +143,29 @@ class BaseField(object):
if instance._initialised:
try:
if (self.name not in instance._data or
instance._data[self.name] != value):
value_has_changed = (
self.name not in instance._data
or instance._data[self.name] != value
)
if value_has_changed:
instance._mark_as_changed(self.name)
except Exception:
# Values cant be compared eg: naive and tz datetimes
# So mark it as changed
# Some values can't be compared and throw an error when we
# attempt to do so (e.g. tz-naive and tz-aware datetimes).
# Mark the field as changed in such cases.
instance._mark_as_changed(self.name)
EmbeddedDocument = _import_class('EmbeddedDocument')
EmbeddedDocument = _import_class("EmbeddedDocument")
if isinstance(value, EmbeddedDocument):
value._instance = weakref.proxy(instance)
elif isinstance(value, (list, tuple)):
for v in value:
if isinstance(v, EmbeddedDocument):
v._instance = weakref.proxy(instance)
instance._data[self.name] = value
def error(self, message='', errors=None, field_name=None):
def error(self, message="", errors=None, field_name=None):
"""Raise a ValidationError."""
field_name = field_name if field_name else self.name
raise ValidationError(message, errors=errors, field_name=field_name)
@ -170,11 +182,11 @@ class BaseField(object):
"""Helper method to call to_mongo with proper inputs."""
f_inputs = self.to_mongo.__code__.co_varnames
ex_vars = {}
if 'fields' in f_inputs:
ex_vars['fields'] = fields
if "fields" in f_inputs:
ex_vars["fields"] = fields
if 'use_db_field' in f_inputs:
ex_vars['use_db_field'] = use_db_field
if "use_db_field" in f_inputs:
ex_vars["use_db_field"] = use_db_field
return self.to_mongo(value, **ex_vars)
@ -189,8 +201,8 @@ class BaseField(object):
pass
def _validate_choices(self, value):
Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument')
Document = _import_class("Document")
EmbeddedDocument = _import_class("EmbeddedDocument")
choice_list = self.choices
if isinstance(next(iter(choice_list)), (list, tuple)):
@ -200,14 +212,12 @@ class BaseField(object):
# Choices which are other types of Documents
if isinstance(value, (Document, EmbeddedDocument)):
if not any(isinstance(value, c) for c in choice_list):
self.error(
'Value must be an instance of %s' % (
six.text_type(choice_list)
)
)
self.error("Value must be an instance of %s" % (choice_list))
# Choices which are types other than Documents
elif value not in choice_list:
self.error('Value must be one of %s' % six.text_type(choice_list))
else:
values = value if isinstance(value, (list, tuple)) else [value]
if len(set(values) - set(choice_list)):
self.error("Value must be one of %s" % str(choice_list))
def _validate(self, value, **kwargs):
# Check the Choices Constraint
@ -217,11 +227,23 @@ class BaseField(object):
# check validation argument
if self.validation is not None:
if callable(self.validation):
if not self.validation(value):
self.error('Value does not match custom validation method')
try:
# breaking change of 0.18
# Get rid of True/False-type return for the validation method
# in favor of having validation raising a ValidationError
ret = self.validation(value)
if ret is not None:
raise DeprecatedError(
"validation argument for `%s` must not return anything, "
"it should raise a ValidationError if validation fails"
% self.name
)
except ValidationError as ex:
self.error(str(ex))
else:
raise ValueError('validation argument for "%s" must be a '
'callable.' % self.name)
raise ValueError(
'validation argument for `"%s"` must be a ' "callable." % self.name
)
self.validate(value, **kwargs)
@ -243,11 +265,22 @@ class ComplexBaseField(BaseField):
Allows for nesting of embedded documents inside complex types.
Handles the lazy dereferencing of a queryset by lazily dereferencing all
items in a list / dict rather than one at a time.
.. versionadded:: 0.5
"""
field = None
def __init__(self, field=None, **kwargs):
self.field = field
super().__init__(**kwargs)
@staticmethod
def _lazy_load_refs(instance, name, ref_values, *, max_depth):
_dereference = _import_class("DeReference")()
documents = _dereference(
ref_values,
max_depth=max_depth,
instance=instance,
name=name,
)
return documents
def __get__(self, instance, owner):
"""Descriptor to automatically dereference references."""
@ -255,28 +288,37 @@ class ComplexBaseField(BaseField):
# Document class being used rather than a document object
return self
ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class('GenericReferenceField')
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
dereference = (self._auto_dereference and
(self.field is None or isinstance(self.field,
(GenericReferenceField, ReferenceField))))
ReferenceField = _import_class("ReferenceField")
GenericReferenceField = _import_class("GenericReferenceField")
EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField")
_dereference = _import_class('DeReference')()
auto_dereference = instance._fields[self.name]._auto_dereference
self._auto_dereference = instance._fields[self.name]._auto_dereference
if instance._initialised and dereference and instance._data.get(self.name):
instance._data[self.name] = _dereference(
instance._data.get(self.name), max_depth=1, instance=instance,
name=self.name
dereference = auto_dereference and (
self.field is None
or isinstance(self.field, (GenericReferenceField, ReferenceField))
)
if (
instance._initialised
and dereference
and instance._data.get(self.name)
and not getattr(instance._data[self.name], "_dereferenced", False)
):
ref_values = instance._data.get(self.name)
instance._data[self.name] = self._lazy_load_refs(
ref_values=ref_values, instance=instance, name=self.name, max_depth=1
)
if hasattr(instance._data[self.name], "_dereferenced"):
instance._data[self.name]._dereferenced = True
value = super(ComplexBaseField, self).__get__(instance, owner)
value = super().__get__(instance, owner)
# Convert lists / values so we can watch for any changes on them
if isinstance(value, (list, tuple)):
if (issubclass(type(self), EmbeddedDocumentListField) and
not isinstance(value, EmbeddedDocumentList)):
if issubclass(type(self), EmbeddedDocumentListField) and not isinstance(
value, EmbeddedDocumentList
):
value = EmbeddedDocumentList(value, instance, self.name)
elif not isinstance(value, BaseList):
value = BaseList(value, instance, self.name)
@ -285,11 +327,14 @@ class ComplexBaseField(BaseField):
value = BaseDict(value, instance, self.name)
instance._data[self.name] = value
if (self._auto_dereference and instance._initialised and
isinstance(value, (BaseList, BaseDict)) and
not value._dereferenced):
value = _dereference(
value, max_depth=1, instance=instance, name=self.name
if (
auto_dereference
and instance._initialised
and isinstance(value, (BaseList, BaseDict))
and not value._dereferenced
):
value = self._lazy_load_refs(
ref_values=value, instance=instance, name=self.name, max_depth=1
)
value._dereferenced = True
instance._data[self.name] = value
@ -298,66 +343,75 @@ class ComplexBaseField(BaseField):
def to_python(self, value):
"""Convert a MongoDB-compatible type to a Python type."""
if isinstance(value, six.string_types):
if isinstance(value, str):
return value
if hasattr(value, 'to_python'):
if hasattr(value, "to_python"):
return value.to_python()
BaseDocument = _import_class("BaseDocument")
if isinstance(value, BaseDocument):
# Something is wrong, return the value as it is
return value
is_list = False
if not hasattr(value, 'items'):
if not hasattr(value, "items"):
try:
is_list = True
value = {k: v for k, v in enumerate(value)}
value = {idx: v for idx, v in enumerate(value)}
except TypeError: # Not iterable return the value
return value
if self.field:
self.field._auto_dereference = self._auto_dereference
value_dict = {key: self.field.to_python(item)
for key, item in value.items()}
value_dict = {
key: self.field.to_python(item) for key, item in value.items()
}
else:
Document = _import_class('Document')
Document = _import_class("Document")
value_dict = {}
for k, v in value.items():
if isinstance(v, Document):
# We need the id from the saved object to create the DBRef
if v.pk is None:
self.error('You can only reference documents once they'
' have been saved to the database')
self.error(
"You can only reference documents once they"
" have been saved to the database"
)
collection = v._get_collection_name()
value_dict[k] = DBRef(collection, v.pk)
elif hasattr(v, 'to_python'):
elif hasattr(v, "to_python"):
value_dict[k] = v.to_python()
else:
value_dict[k] = self.to_python(v)
if is_list: # Convert back to a list
return [v for _, v in sorted(value_dict.items(),
key=operator.itemgetter(0))]
return [
v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0))
]
return value_dict
def to_mongo(self, value, use_db_field=True, fields=None):
"""Convert a Python type to a MongoDB-compatible type."""
Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument')
GenericReferenceField = _import_class('GenericReferenceField')
Document = _import_class("Document")
EmbeddedDocument = _import_class("EmbeddedDocument")
GenericReferenceField = _import_class("GenericReferenceField")
if isinstance(value, six.string_types):
if isinstance(value, str):
return value
if hasattr(value, 'to_mongo'):
if hasattr(value, "to_mongo"):
if isinstance(value, Document):
return GenericReferenceField().to_mongo(value)
cls = value.__class__
val = value.to_mongo(use_db_field, fields)
# If it's a document that is not inherited add _cls
if isinstance(value, EmbeddedDocument):
val['_cls'] = cls.__name__
val["_cls"] = cls.__name__
return val
is_list = False
if not hasattr(value, 'items'):
if not hasattr(value, "items"):
try:
is_list = True
value = {k: v for k, v in enumerate(value)}
@ -367,48 +421,51 @@ class ComplexBaseField(BaseField):
if self.field:
value_dict = {
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
for key, item in value.iteritems()
for key, item in value.items()
}
else:
value_dict = {}
for k, v in value.iteritems():
for k, v in value.items():
if isinstance(v, Document):
# We need the id from the saved object to create the DBRef
if v.pk is None:
self.error('You can only reference documents once they'
' have been saved to the database')
self.error(
"You can only reference documents once they"
" have been saved to the database"
)
# If its a document that is not inheritable it won't have
# any _cls data so make it a generic reference allows
# us to dereference
meta = getattr(v, '_meta', {})
allow_inheritance = meta.get('allow_inheritance')
meta = getattr(v, "_meta", {})
allow_inheritance = meta.get("allow_inheritance")
if not allow_inheritance and not self.field:
value_dict[k] = GenericReferenceField().to_mongo(v)
else:
collection = v._get_collection_name()
value_dict[k] = DBRef(collection, v.pk)
elif hasattr(v, 'to_mongo'):
elif hasattr(v, "to_mongo"):
cls = v.__class__
val = v.to_mongo(use_db_field, fields)
# If it's a document that is not inherited add _cls
if isinstance(v, (Document, EmbeddedDocument)):
val['_cls'] = cls.__name__
val["_cls"] = cls.__name__
value_dict[k] = val
else:
value_dict[k] = self.to_mongo(v, use_db_field, fields)
if is_list: # Convert back to a list
return [v for _, v in sorted(value_dict.items(),
key=operator.itemgetter(0))]
return [
v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0))
]
return value_dict
def validate(self, value):
"""If field is provided ensure the value is valid."""
errors = {}
if self.field:
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
sequence = value.iteritems()
if hasattr(value, "items"):
sequence = value.items()
else:
sequence = enumerate(value)
for k, v in sequence:
@ -421,11 +478,10 @@ class ComplexBaseField(BaseField):
if errors:
field_class = self.field.__class__.__name__
self.error('Invalid %s item (%s)' % (field_class, value),
errors=errors)
self.error(f"Invalid {field_class} item ({value})", errors=errors)
# Don't allow empty values if required
if self.required and not value:
self.error('Field is required and cannot be empty')
self.error("Field is required and cannot be empty")
def prepare_query_value(self, op, value):
return self.to_mongo(value)
@ -455,10 +511,9 @@ class ObjectIdField(BaseField):
def to_mongo(self, value):
if not isinstance(value, ObjectId):
try:
return ObjectId(six.text_type(value))
return ObjectId(str(value))
except Exception as e:
# e.message attribute has been deprecated since Python 2.6
self.error(six.text_type(e))
self.error(str(e))
return value
def prepare_query_value(self, op, value):
@ -466,84 +521,82 @@ class ObjectIdField(BaseField):
def validate(self, value):
try:
ObjectId(six.text_type(value))
ObjectId(str(value))
except Exception:
self.error('Invalid Object ID')
self.error("Invalid ObjectID")
class GeoJsonBaseField(BaseField):
"""A geo json field storing a geojson style object.
.. versionadded:: 0.8
"""
"""A geo json field storing a geojson style object."""
_geo_index = pymongo.GEOSPHERE
_type = 'GeoBase'
_type = "GeoBase"
def __init__(self, auto_index=True, *args, **kwargs):
"""
:param bool auto_index: Automatically create a '2dsphere' index.\
Defaults to `True`.
"""
self._name = '%sField' % self._type
self._name = "%sField" % self._type
if not auto_index:
self._geo_index = False
super(GeoJsonBaseField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
def validate(self, value):
"""Validate the GeoJson object based on its type."""
if isinstance(value, dict):
if set(value.keys()) == set(['type', 'coordinates']):
if value['type'] != self._type:
self.error('%s type must be "%s"' %
(self._name, self._type))
return self.validate(value['coordinates'])
if set(value.keys()) == {"type", "coordinates"}:
if value["type"] != self._type:
self.error(f'{self._name} type must be "{self._type}"')
return self.validate(value["coordinates"])
else:
self.error('%s can only accept a valid GeoJson dictionary'
' or lists of (x, y)' % self._name)
self.error(
"%s can only accept a valid GeoJson dictionary"
" or lists of (x, y)" % self._name
)
return
elif not isinstance(value, (list, tuple)):
self.error('%s can only accept lists of [x, y]' % self._name)
self.error("%s can only accept lists of [x, y]" % self._name)
return
validate = getattr(self, '_validate_%s' % self._type.lower())
validate = getattr(self, "_validate_%s" % self._type.lower())
error = validate(value)
if error:
self.error(error)
def _validate_polygon(self, value, top_level=True):
if not isinstance(value, (list, tuple)):
return 'Polygons must contain list of linestrings'
return "Polygons must contain list of linestrings"
# Quick and dirty validator
try:
value[0][0][0]
except (TypeError, IndexError):
return 'Invalid Polygon must contain at least one valid linestring'
return "Invalid Polygon must contain at least one valid linestring"
errors = []
for val in value:
error = self._validate_linestring(val, False)
if not error and val[0] != val[-1]:
error = 'LineStrings must start and end at the same point'
error = "LineStrings must start and end at the same point"
if error and error not in errors:
errors.append(error)
if errors:
if top_level:
return 'Invalid Polygon:\n%s' % ', '.join(errors)
return "Invalid Polygon:\n%s" % ", ".join(errors)
else:
return '%s' % ', '.join(errors)
return "%s" % ", ".join(errors)
def _validate_linestring(self, value, top_level=True):
"""Validate a linestring."""
if not isinstance(value, (list, tuple)):
return 'LineStrings must contain list of coordinate pairs'
return "LineStrings must contain list of coordinate pairs"
# Quick and dirty validator
try:
value[0][0]
except (TypeError, IndexError):
return 'Invalid LineString must contain at least one valid point'
return "Invalid LineString must contain at least one valid point"
errors = []
for val in value:
@ -552,29 +605,30 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
if top_level:
return 'Invalid LineString:\n%s' % ', '.join(errors)
return "Invalid LineString:\n%s" % ", ".join(errors)
else:
return '%s' % ', '.join(errors)
return "%s" % ", ".join(errors)
def _validate_point(self, value):
"""Validate each set of coords"""
if not isinstance(value, (list, tuple)):
return 'Points must be a list of coordinate pairs'
return "Points must be a list of coordinate pairs"
elif not len(value) == 2:
return 'Value (%s) must be a two-dimensional point' % repr(value)
elif (not isinstance(value[0], (float, int)) or
not isinstance(value[1], (float, int))):
return 'Both values (%s) in point must be float or int' % repr(value)
return "Value (%s) must be a two-dimensional point" % repr(value)
elif not isinstance(value[0], (float, int)) or not isinstance(
value[1], (float, int)
):
return "Both values (%s) in point must be float or int" % repr(value)
def _validate_multipoint(self, value):
if not isinstance(value, (list, tuple)):
return 'MultiPoint must be a list of Point'
return "MultiPoint must be a list of Point"
# Quick and dirty validator
try:
value[0][0]
except (TypeError, IndexError):
return 'Invalid MultiPoint must contain at least one valid point'
return "Invalid MultiPoint must contain at least one valid point"
errors = []
for point in value:
@ -583,17 +637,17 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
return '%s' % ', '.join(errors)
return "%s" % ", ".join(errors)
def _validate_multilinestring(self, value, top_level=True):
if not isinstance(value, (list, tuple)):
return 'MultiLineString must be a list of LineString'
return "MultiLineString must be a list of LineString"
# Quick and dirty validator
try:
value[0][0][0]
except (TypeError, IndexError):
return 'Invalid MultiLineString must contain at least one valid linestring'
return "Invalid MultiLineString must contain at least one valid linestring"
errors = []
for linestring in value:
@ -603,19 +657,19 @@ class GeoJsonBaseField(BaseField):
if errors:
if top_level:
return 'Invalid MultiLineString:\n%s' % ', '.join(errors)
return "Invalid MultiLineString:\n%s" % ", ".join(errors)
else:
return '%s' % ', '.join(errors)
return "%s" % ", ".join(errors)
def _validate_multipolygon(self, value):
if not isinstance(value, (list, tuple)):
return 'MultiPolygon must be a list of Polygon'
return "MultiPolygon must be a list of Polygon"
# Quick and dirty validator
try:
value[0][0][0][0]
except (TypeError, IndexError):
return 'Invalid MultiPolygon must contain at least one valid Polygon'
return "Invalid MultiPolygon must contain at least one valid Polygon"
errors = []
for polygon in value:
@ -624,9 +678,9 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
return 'Invalid MultiPolygon:\n%s' % ', '.join(errors)
return "Invalid MultiPolygon:\n%s" % ", ".join(errors)
def to_mongo(self, value):
if isinstance(value, dict):
return value
return SON([('type', self._type), ('coordinates', value)])
return SON([("type", self._type), ("coordinates", value)])

View File

@ -1,68 +1,75 @@
import itertools
import warnings
import six
from mongoengine.base.common import _document_registry
from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
from mongoengine.base.fields import (
BaseField,
ComplexBaseField,
ObjectIdField,
)
from mongoengine.common import _import_class
from mongoengine.errors import InvalidDocumentError
from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
MultipleObjectsReturned,
QuerySetManager)
from mongoengine.queryset import (
DO_NOTHING,
DoesNotExist,
MultipleObjectsReturned,
QuerySetManager,
)
__all__ = ('DocumentMetaclass', 'TopLevelDocumentMetaclass')
__all__ = ("DocumentMetaclass", "TopLevelDocumentMetaclass")
class DocumentMetaclass(type):
"""Metaclass for all documents."""
# TODO lower complexity of this method
def __new__(cls, name, bases, attrs):
flattened_bases = cls._get_bases(bases)
super_new = super(DocumentMetaclass, cls).__new__
def __new__(mcs, name, bases, attrs):
flattened_bases = mcs._get_bases(bases)
super_new = super().__new__
# If a base class just call super
metaclass = attrs.get('my_metaclass')
metaclass = attrs.get("my_metaclass")
if metaclass and issubclass(metaclass, DocumentMetaclass):
return super_new(cls, name, bases, attrs)
return super_new(mcs, name, bases, attrs)
attrs['_is_document'] = attrs.get('_is_document', False)
attrs['_cached_reference_fields'] = []
attrs["_is_document"] = attrs.get("_is_document", False)
attrs["_cached_reference_fields"] = []
# EmbeddedDocuments could have meta data for inheritance
if 'meta' in attrs:
attrs['_meta'] = attrs.pop('meta')
if "meta" in attrs:
attrs["_meta"] = attrs.pop("meta")
# EmbeddedDocuments should inherit meta data
if '_meta' not in attrs:
if "_meta" not in attrs:
meta = MetaDict()
for base in flattened_bases[::-1]:
# Add any mixin metadata from plain objects
if hasattr(base, 'meta'):
if hasattr(base, "meta"):
meta.merge(base.meta)
elif hasattr(base, '_meta'):
elif hasattr(base, "_meta"):
meta.merge(base._meta)
attrs['_meta'] = meta
attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
attrs["_meta"] = meta
attrs["_meta"][
"abstract"
] = False # 789: EmbeddedDocument shouldn't inherit abstract
# If allow_inheritance is True, add a "_cls" string field to the attrs
if attrs['_meta'].get('allow_inheritance'):
StringField = _import_class('StringField')
attrs['_cls'] = StringField()
if attrs["_meta"].get("allow_inheritance"):
StringField = _import_class("StringField")
attrs["_cls"] = StringField()
# Handle document Fields
# Merge all fields from subclasses
doc_fields = {}
for base in flattened_bases[::-1]:
if hasattr(base, '_fields'):
if hasattr(base, "_fields"):
doc_fields.update(base._fields)
# Standard object mixin - merge in any Fields
if not hasattr(base, '_meta'):
if not hasattr(base, "_meta"):
base_fields = {}
for attr_name, attr_value in base.__dict__.iteritems():
for attr_name, attr_value in base.__dict__.items():
if not isinstance(attr_value, BaseField):
continue
attr_value.name = attr_name
@ -74,7 +81,7 @@ class DocumentMetaclass(type):
# Discover any document fields
field_names = {}
for attr_name, attr_value in attrs.iteritems():
for attr_name, attr_value in attrs.items():
if not isinstance(attr_value, BaseField):
continue
attr_value.name = attr_name
@ -83,27 +90,29 @@ class DocumentMetaclass(type):
doc_fields[attr_name] = attr_value
# Count names to ensure no db_field redefinitions
field_names[attr_value.db_field] = field_names.get(
attr_value.db_field, 0) + 1
field_names[attr_value.db_field] = (
field_names.get(attr_value.db_field, 0) + 1
)
# Ensure no duplicate db_fields
duplicate_db_fields = [k for k, v in field_names.items() if v > 1]
if duplicate_db_fields:
msg = ('Multiple db_fields defined for: %s ' %
', '.join(duplicate_db_fields))
msg = "Multiple db_fields defined for: %s " % ", ".join(duplicate_db_fields)
raise InvalidDocumentError(msg)
# Set _fields and db_field maps
attrs['_fields'] = doc_fields
attrs['_db_field_map'] = {k: getattr(v, 'db_field', k)
for k, v in doc_fields.items()}
attrs['_reverse_db_field_map'] = {
v: k for k, v in attrs['_db_field_map'].items()
attrs["_fields"] = doc_fields
attrs["_db_field_map"] = {
k: getattr(v, "db_field", k) for k, v in doc_fields.items()
}
attrs["_reverse_db_field_map"] = {
v: k for k, v in attrs["_db_field_map"].items()
}
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
(v.creation_counter, v.name)
for v in doc_fields.itervalues()))
attrs["_fields_ordered"] = tuple(
i[1]
for i in sorted((v.creation_counter, v.name) for v in doc_fields.values())
)
#
# Set document hierarchy
@ -111,34 +120,37 @@ class DocumentMetaclass(type):
superclasses = ()
class_name = [name]
for base in flattened_bases:
if (not getattr(base, '_is_base_cls', True) and
not getattr(base, '_meta', {}).get('abstract', True)):
if not getattr(base, "_is_base_cls", True) and not getattr(
base, "_meta", {}
).get("abstract", True):
# Collate hierarchy for _cls and _subclasses
class_name.append(base.__name__)
if hasattr(base, '_meta'):
if hasattr(base, "_meta"):
# Warn if allow_inheritance isn't set and prevent
# inheritance of classes where inheritance is set to False
allow_inheritance = base._meta.get('allow_inheritance')
if not allow_inheritance and not base._meta.get('abstract'):
raise ValueError('Document %s may not be subclassed' %
base.__name__)
allow_inheritance = base._meta.get("allow_inheritance")
if not allow_inheritance and not base._meta.get("abstract"):
raise ValueError(
"Document %s may not be subclassed. "
'To enable inheritance, use the "allow_inheritance" meta attribute.'
% base.__name__
)
# Get superclasses from last base superclass
document_bases = [b for b in flattened_bases
if hasattr(b, '_class_name')]
document_bases = [b for b in flattened_bases if hasattr(b, "_class_name")]
if document_bases:
superclasses = document_bases[0]._superclasses
superclasses += (document_bases[0]._class_name, )
superclasses += (document_bases[0]._class_name,)
_cls = '.'.join(reversed(class_name))
attrs['_class_name'] = _cls
attrs['_superclasses'] = superclasses
attrs['_subclasses'] = (_cls, )
attrs['_types'] = attrs['_subclasses'] # TODO depreciate _types
_cls = ".".join(reversed(class_name))
attrs["_class_name"] = _cls
attrs["_superclasses"] = superclasses
attrs["_subclasses"] = (_cls,)
attrs["_types"] = attrs["_subclasses"] # TODO depreciate _types
# Create the new_class
new_class = super_new(cls, name, bases, attrs)
new_class = super_new(mcs, name, bases, attrs)
# Set _subclasses
for base in document_bases:
@ -146,8 +158,12 @@ class DocumentMetaclass(type):
base._subclasses += (_cls,)
base._types = base._subclasses # TODO depreciate _types
(Document, EmbeddedDocument, DictField,
CachedReferenceField) = cls._import_classes()
(
Document,
EmbeddedDocument,
DictField,
CachedReferenceField,
) = mcs._import_classes()
if issubclass(new_class, Document):
new_class._collection = None
@ -155,97 +171,77 @@ class DocumentMetaclass(type):
# Add class to the _document_registry
_document_registry[new_class._class_name] = new_class
# In Python 2, User-defined methods objects have special read-only
# attributes 'im_func' and 'im_self' which contain the function obj
# and class instance object respectively. With Python 3 these special
# attributes have been replaced by __func__ and __self__. The Blinker
# module continues to use im_func and im_self, so the code below
# copies __func__ into im_func and __self__ into im_self for
# classmethod objects in Document derived classes.
if six.PY3:
for val in new_class.__dict__.values():
if isinstance(val, classmethod):
f = val.__get__(new_class)
if hasattr(f, '__func__') and not hasattr(f, 'im_func'):
f.__dict__.update({'im_func': getattr(f, '__func__')})
if hasattr(f, '__self__') and not hasattr(f, 'im_self'):
f.__dict__.update({'im_self': getattr(f, '__self__')})
# Handle delete rules
for field in new_class._fields.itervalues():
for field in new_class._fields.values():
f = field
if f.owner_document is None:
f.owner_document = new_class
delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING)
delete_rule = getattr(f, "reverse_delete_rule", DO_NOTHING)
if isinstance(f, CachedReferenceField):
if issubclass(new_class, EmbeddedDocument):
raise InvalidDocumentError('CachedReferenceFields is not '
'allowed in EmbeddedDocuments')
if not f.document_type:
raise InvalidDocumentError(
'Document is not available to sync')
"CachedReferenceFields is not allowed in EmbeddedDocuments"
)
if f.auto_sync:
f.start_listener()
f.document_type._cached_reference_fields.append(f)
if isinstance(f, ComplexBaseField) and hasattr(f, 'field'):
delete_rule = getattr(f.field,
'reverse_delete_rule',
DO_NOTHING)
if isinstance(f, ComplexBaseField) and hasattr(f, "field"):
delete_rule = getattr(f.field, "reverse_delete_rule", DO_NOTHING)
if isinstance(f, DictField) and delete_rule != DO_NOTHING:
msg = ('Reverse delete rules are not supported '
'for %s (field: %s)' %
(field.__class__.__name__, field.name))
msg = (
"Reverse delete rules are not supported "
"for %s (field: %s)" % (field.__class__.__name__, field.name)
)
raise InvalidDocumentError(msg)
f = field.field
if delete_rule != DO_NOTHING:
if issubclass(new_class, EmbeddedDocument):
msg = ('Reverse delete rules are not supported for '
'EmbeddedDocuments (field: %s)' % field.name)
msg = (
"Reverse delete rules are not supported for "
"EmbeddedDocuments (field: %s)" % field.name
)
raise InvalidDocumentError(msg)
f.document_type.register_delete_rule(new_class,
field.name, delete_rule)
f.document_type.register_delete_rule(new_class, field.name, delete_rule)
if (field.name and hasattr(Document, field.name) and
EmbeddedDocument not in new_class.mro()):
msg = ('%s is a document method and not a valid '
'field name' % field.name)
if (
field.name
and hasattr(Document, field.name)
and EmbeddedDocument not in new_class.mro()
):
msg = "%s is a document method and not a valid field name" % field.name
raise InvalidDocumentError(msg)
return new_class
def add_to_class(self, name, value):
setattr(self, name, value)
@classmethod
def _get_bases(cls, bases):
def _get_bases(mcs, bases):
if isinstance(bases, BasesTuple):
return bases
seen = []
bases = cls.__get_bases(bases)
bases = mcs.__get_bases(bases)
unique_bases = (b for b in bases if not (b in seen or seen.append(b)))
return BasesTuple(unique_bases)
@classmethod
def __get_bases(cls, bases):
def __get_bases(mcs, bases):
for base in bases:
if base is object:
continue
yield base
for child_base in cls.__get_bases(base.__bases__):
yield child_base
yield from mcs.__get_bases(base.__bases__)
@classmethod
def _import_classes(cls):
Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument')
DictField = _import_class('DictField')
CachedReferenceField = _import_class('CachedReferenceField')
def _import_classes(mcs):
Document = _import_class("Document")
EmbeddedDocument = _import_class("EmbeddedDocument")
DictField = _import_class("DictField")
CachedReferenceField = _import_class("CachedReferenceField")
return Document, EmbeddedDocument, DictField, CachedReferenceField
@ -254,193 +250,213 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
collection in the database.
"""
def __new__(cls, name, bases, attrs):
flattened_bases = cls._get_bases(bases)
super_new = super(TopLevelDocumentMetaclass, cls).__new__
def __new__(mcs, name, bases, attrs):
flattened_bases = mcs._get_bases(bases)
super_new = super().__new__
# Set default _meta data if base class, otherwise get user defined meta
if attrs.get('my_metaclass') == TopLevelDocumentMetaclass:
if attrs.get("my_metaclass") == TopLevelDocumentMetaclass:
# defaults
attrs['_meta'] = {
'abstract': True,
'max_documents': None,
'max_size': None,
'ordering': [], # default ordering applied at runtime
'indexes': [], # indexes to be ensured at runtime
'id_field': None,
'index_background': False,
'index_drop_dups': False,
'index_opts': None,
'delete_rules': None,
attrs["_meta"] = {
"abstract": True,
"max_documents": None,
"max_size": None,
"ordering": [], # default ordering applied at runtime
"indexes": [], # indexes to be ensured at runtime
"id_field": None,
"index_background": False,
"index_opts": None,
"delete_rules": None,
# allow_inheritance can be True, False, and None. True means
# "allow inheritance", False means "don't allow inheritance",
# None means "do whatever your parent does, or don't allow
# inheritance if you're a top-level class".
'allow_inheritance': None,
"allow_inheritance": None,
}
attrs['_is_base_cls'] = True
attrs['_meta'].update(attrs.get('meta', {}))
attrs["_is_base_cls"] = True
attrs["_meta"].update(attrs.get("meta", {}))
else:
attrs['_meta'] = attrs.get('meta', {})
attrs["_meta"] = attrs.get("meta", {})
# Explicitly set abstract to false unless set
attrs['_meta']['abstract'] = attrs['_meta'].get('abstract', False)
attrs['_is_base_cls'] = False
attrs["_meta"]["abstract"] = attrs["_meta"].get("abstract", False)
attrs["_is_base_cls"] = False
# Set flag marking as document class - as opposed to an object mixin
attrs['_is_document'] = True
attrs["_is_document"] = True
# Ensure queryset_class is inherited
if 'objects' in attrs:
manager = attrs['objects']
if hasattr(manager, 'queryset_class'):
attrs['_meta']['queryset_class'] = manager.queryset_class
if "objects" in attrs:
manager = attrs["objects"]
if hasattr(manager, "queryset_class"):
attrs["_meta"]["queryset_class"] = manager.queryset_class
# Clean up top level meta
if 'meta' in attrs:
del attrs['meta']
if "meta" in attrs:
del attrs["meta"]
# Find the parent document class
parent_doc_cls = [b for b in flattened_bases
if b.__class__ == TopLevelDocumentMetaclass]
parent_doc_cls = [
b for b in flattened_bases if b.__class__ == TopLevelDocumentMetaclass
]
parent_doc_cls = None if not parent_doc_cls else parent_doc_cls[0]
# Prevent classes setting collection different to their parents
# If parent wasn't an abstract class
if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
not parent_doc_cls._meta.get('abstract', True)):
msg = 'Trying to set a collection on a subclass (%s)' % name
if (
parent_doc_cls
and "collection" in attrs.get("_meta", {})
and not parent_doc_cls._meta.get("abstract", True)
):
msg = "Trying to set a collection on a subclass (%s)" % name
warnings.warn(msg, SyntaxWarning)
del attrs['_meta']['collection']
del attrs["_meta"]["collection"]
# Ensure abstract documents have abstract bases
if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'):
if (parent_doc_cls and
not parent_doc_cls._meta.get('abstract', False)):
msg = 'Abstract document cannot have non-abstract base'
if attrs.get("_is_base_cls") or attrs["_meta"].get("abstract"):
if parent_doc_cls and not parent_doc_cls._meta.get("abstract", False):
msg = "Abstract document cannot have non-abstract base"
raise ValueError(msg)
return super_new(cls, name, bases, attrs)
return super_new(mcs, name, bases, attrs)
# Merge base class metas.
# Uses a special MetaDict that handles various merging rules
meta = MetaDict()
for base in flattened_bases[::-1]:
# Add any mixin metadata from plain objects
if hasattr(base, 'meta'):
if hasattr(base, "meta"):
meta.merge(base.meta)
elif hasattr(base, '_meta'):
elif hasattr(base, "_meta"):
meta.merge(base._meta)
# Set collection in the meta if its callable
if (getattr(base, '_is_document', False) and
not base._meta.get('abstract')):
collection = meta.get('collection', None)
if getattr(base, "_is_document", False) and not base._meta.get("abstract"):
collection = meta.get("collection", None)
if callable(collection):
meta['collection'] = collection(base)
meta["collection"] = collection(base)
meta.merge(attrs.get('_meta', {})) # Top level meta
meta.merge(attrs.get("_meta", {})) # Top level meta
# Only simple classes (i.e. direct subclasses of Document) may set
# allow_inheritance to False. If the base Document allows inheritance,
# none of its subclasses can override allow_inheritance to False.
simple_class = all([b._meta.get('abstract')
for b in flattened_bases if hasattr(b, '_meta')])
simple_class = all(
b._meta.get("abstract") for b in flattened_bases if hasattr(b, "_meta")
)
if (
not simple_class and
meta['allow_inheritance'] is False and
not meta['abstract']
not simple_class
and meta["allow_inheritance"] is False
and not meta["abstract"]
):
raise ValueError('Only direct subclasses of Document may set '
'"allow_inheritance" to False')
raise ValueError(
"Only direct subclasses of Document may set "
'"allow_inheritance" to False'
)
# Set default collection name
if 'collection' not in meta:
meta['collection'] = ''.join('_%s' % c if c.isupper() else c
for c in name).strip('_').lower()
attrs['_meta'] = meta
if "collection" not in meta:
meta["collection"] = (
"".join("_%s" % c if c.isupper() else c for c in name)
.strip("_")
.lower()
)
attrs["_meta"] = meta
# Call super and get the new class
new_class = super_new(cls, name, bases, attrs)
new_class = super_new(mcs, name, bases, attrs)
meta = new_class._meta
# Set index specifications
meta['index_specs'] = new_class._build_index_specs(meta['indexes'])
meta["index_specs"] = new_class._build_index_specs(meta["indexes"])
# If collection is a callable - call it and set the value
collection = meta.get('collection')
collection = meta.get("collection")
if callable(collection):
new_class._meta['collection'] = collection(new_class)
new_class._meta["collection"] = collection(new_class)
# Provide a default queryset unless exists or one has been set
if 'objects' not in dir(new_class):
if "objects" not in dir(new_class):
new_class.objects = QuerySetManager()
# Validate the fields and set primary key if needed
for field_name, field in new_class._fields.iteritems():
for field_name, field in new_class._fields.items():
if field.primary_key:
# Ensure only one primary key is set
current_pk = new_class._meta.get('id_field')
current_pk = new_class._meta.get("id_field")
if current_pk and current_pk != field_name:
raise ValueError('Cannot override primary key field')
raise ValueError("Cannot override primary key field")
# Set primary key
if not current_pk:
new_class._meta['id_field'] = field_name
new_class._meta["id_field"] = field_name
new_class.id = field
# Set primary key if not defined by the document
new_class._auto_id_field = getattr(parent_doc_cls,
'_auto_id_field', False)
if not new_class._meta.get('id_field'):
# After 0.10, find not existing names, instead of overwriting
id_name, id_db_name = cls.get_auto_id_names(new_class)
new_class._auto_id_field = True
new_class._meta['id_field'] = id_name
# If the document doesn't explicitly define a primary key field, create
# one. Make it an ObjectIdField and give it a non-clashing name ("id"
# by default, but can be different if that one's taken).
if not new_class._meta.get("id_field"):
id_name, id_db_name = mcs.get_auto_id_names(new_class)
new_class._meta["id_field"] = id_name
new_class._fields[id_name] = ObjectIdField(db_field=id_db_name)
new_class._fields[id_name].name = id_name
new_class.id = new_class._fields[id_name]
new_class._db_field_map[id_name] = id_db_name
new_class._reverse_db_field_map[id_db_name] = id_name
# Prepend id field to _fields_ordered
new_class._fields_ordered = (id_name, ) + new_class._fields_ordered
# Merge in exceptions with parent hierarchy
# Prepend the ID field to _fields_ordered (so that it's *always*
# the first field).
new_class._fields_ordered = (id_name,) + new_class._fields_ordered
# Merge in exceptions with parent hierarchy.
exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned)
module = attrs.get('__module__')
module = attrs.get("__module__")
for exc in exceptions_to_merge:
name = exc.__name__
parents = tuple(getattr(base, name) for base in flattened_bases
if hasattr(base, name)) or (exc,)
# Create new exception and set to new_class
exception = type(name, parents, {'__module__': module})
parents = tuple(
getattr(base, name) for base in flattened_bases if hasattr(base, name)
) or (exc,)
# Create a new exception and set it as an attribute on the new
# class.
exception = type(name, parents, {"__module__": module})
setattr(new_class, name, exception)
return new_class
@classmethod
def get_auto_id_names(cls, new_class):
id_name, id_db_name = ('id', '_id')
if id_name not in new_class._fields and \
id_db_name not in (v.db_field for v in new_class._fields.values()):
def get_auto_id_names(mcs, new_class):
"""Find a name for the automatic ID field for the given new class.
Return a two-element tuple where the first item is the field name (i.e.
the attribute name on the object) and the second element is the DB
field name (i.e. the name of the key stored in MongoDB).
Defaults to ('id', '_id'), or generates a non-clashing name in the form
of ('auto_id_X', '_auto_id_X') if the default name is already taken.
"""
id_name, id_db_name = ("id", "_id")
existing_fields = {field_name for field_name in new_class._fields}
existing_db_fields = {v.db_field for v in new_class._fields.values()}
if id_name not in existing_fields and id_db_name not in existing_db_fields:
return id_name, id_db_name
id_basename, id_db_basename, i = 'auto_id', '_auto_id', 0
while id_name in new_class._fields or \
id_db_name in (v.db_field for v in new_class._fields.values()):
id_name = '{0}_{1}'.format(id_basename, i)
id_db_name = '{0}_{1}'.format(id_db_basename, i)
i += 1
return id_name, id_db_name
id_basename, id_db_basename, i = ("auto_id", "_auto_id", 0)
for i in itertools.count():
id_name = f"{id_basename}_{i}"
id_db_name = f"{id_db_basename}_{i}"
if id_name not in existing_fields and id_db_name not in existing_db_fields:
return id_name, id_db_name
class MetaDict(dict):
"""Custom dictionary for meta classes.
Handles the merging of set indexes
"""
_merge_options = ('indexes',)
_merge_options = ("indexes",)
def merge(self, new_options):
for k, v in new_options.iteritems():
for k, v in new_options.items():
if k in self._merge_options:
self[k] = self.get(k, []) + v
else:
@ -449,4 +465,5 @@ class MetaDict(dict):
class BasesTuple(tuple):
"""Special class to handle introspection of bases tuple in __new__"""
pass

22
mongoengine/base/utils.py Normal file
View File

@ -0,0 +1,22 @@
import re
class LazyRegexCompiler:
"""Descriptor to allow lazy compilation of regex"""
def __init__(self, pattern, flags=0):
self._pattern = pattern
self._flags = flags
self._compiled_regex = None
@property
def compiled_regex(self):
if self._compiled_regex is None:
self._compiled_regex = re.compile(self._pattern, self._flags)
return self._compiled_regex
def __get__(self, instance, owner):
return self.compiled_regex
def __set__(self, instance, value):
raise AttributeError("Can not set attribute LazyRegexCompiler")

View File

@ -19,38 +19,44 @@ def _import_class(cls_name):
if cls_name in _class_registry_cache:
return _class_registry_cache.get(cls_name)
doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument',
'MapReduceDocument')
doc_classes = (
"Document",
"DynamicEmbeddedDocument",
"EmbeddedDocument",
"MapReduceDocument",
)
# Field Classes
if not _field_list_cache:
from mongoengine.fields import __all__ as fields
_field_list_cache.extend(fields)
from mongoengine.base.fields import __all__ as fields
_field_list_cache.extend(fields)
field_classes = _field_list_cache
queryset_classes = ('OperationError',)
deref_classes = ('DeReference',)
deref_classes = ("DeReference",)
if cls_name == 'BaseDocument':
if cls_name == "BaseDocument":
from mongoengine.base import document as module
import_classes = ['BaseDocument']
import_classes = ["BaseDocument"]
elif cls_name in doc_classes:
from mongoengine import document as module
import_classes = doc_classes
elif cls_name in field_classes:
from mongoengine import fields as module
import_classes = field_classes
elif cls_name in queryset_classes:
from mongoengine import queryset as module
import_classes = queryset_classes
elif cls_name in deref_classes:
from mongoengine import dereference as module
import_classes = deref_classes
else:
raise ValueError('No import set for: ' % cls_name)
raise ValueError("No import set for: %s" % cls_name)
for cls in import_classes:
_class_registry_cache[cls] = getattr(module, cls)

View File

@ -1,48 +1,68 @@
from pymongo import MongoClient, ReadPreference, uri_parser
import six
from pymongo.database import _check_name
from mongoengine.python_support import IS_PYMONGO_3
__all__ = ['MongoEngineConnectionError', 'connect', 'register_connection',
'DEFAULT_CONNECTION_NAME']
__all__ = [
"DEFAULT_CONNECTION_NAME",
"DEFAULT_DATABASE_NAME",
"ConnectionFailure",
"connect",
"disconnect",
"disconnect_all",
"get_connection",
"get_db",
"register_connection",
]
DEFAULT_CONNECTION_NAME = 'default'
if IS_PYMONGO_3:
READ_PREFERENCE = ReadPreference.PRIMARY
else:
from pymongo import MongoReplicaSetClient
READ_PREFERENCE = False
class MongoEngineConnectionError(Exception):
"""Error raised when the database connection can't be established or
when a connection with a requested alias can't be retrieved.
"""
pass
DEFAULT_CONNECTION_NAME = "default"
DEFAULT_DATABASE_NAME = "test"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 27017
_connection_settings = {}
_connections = {}
_dbs = {}
READ_PREFERENCE = ReadPreference.PRIMARY
def register_connection(alias, name=None, host=None, port=None,
read_preference=READ_PREFERENCE,
username=None, password=None,
authentication_source=None,
authentication_mechanism=None,
**kwargs):
"""Add a connection.
:param alias: the name that will be used to refer to this connection
throughout MongoEngine
class ConnectionFailure(Exception):
"""Error raised when the database connection can't be established or
when a connection with a requested alias can't be retrieved.
"""
pass
def _check_db_name(name):
"""Check if a database name is valid.
This functionality is copied from pymongo Database class constructor.
"""
if not isinstance(name, str):
raise TypeError("name must be an instance of %s" % str)
elif name != "$external":
_check_name(name)
def _get_connection_settings(
db=None,
name=None,
host=None,
port=None,
read_preference=READ_PREFERENCE,
username=None,
password=None,
authentication_source=None,
authentication_mechanism=None,
**kwargs,
):
"""Get the connection settings as a dict
:param db: the name of the database to use, for compatibility with connect
:param name: the name of the specific database to use
:param host: the host name of the :program:`mongod` instance to connect to
:param port: the port that the :program:`mongod` instance is running on
:param host: the host name of the: program: `mongod` instance to connect to
:param port: the port that the: program: `mongod` instance is running on
:param read_preference: The read preference for the collection
** Added pymongo 2.1
:param username: username to authenticate with
:param password: password to authenticate with
:param authentication_source: database to authenticate against
@ -50,79 +70,176 @@ def register_connection(alias, name=None, host=None, port=None,
By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
:param is_mock: explicitly use mongomock for this connection
(can also be done by using `mongomock://` as db host prefix)
(can also be done by using `mongomock: // ` as db host prefix)
:param kwargs: ad-hoc parameters to be passed into the pymongo driver,
for example maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list.
.. versionchanged:: 0.10.6 - added mongomock support
"""
conn_settings = {
'name': name or 'test',
'host': host or 'localhost',
'port': port or 27017,
'read_preference': read_preference,
'username': username,
'password': password,
'authentication_source': authentication_source,
'authentication_mechanism': authentication_mechanism
"name": name or db or DEFAULT_DATABASE_NAME,
"host": host or DEFAULT_HOST,
"port": port or DEFAULT_PORT,
"read_preference": read_preference,
"username": username,
"password": password,
"authentication_source": authentication_source,
"authentication_mechanism": authentication_mechanism,
}
conn_host = conn_settings['host']
_check_db_name(conn_settings["name"])
conn_host = conn_settings["host"]
# Host can be a list or a string, so if string, force to a list.
if isinstance(conn_host, six.string_types):
if isinstance(conn_host, str):
conn_host = [conn_host]
resolved_hosts = []
for entity in conn_host:
# Handle Mongomock
if entity.startswith('mongomock://'):
conn_settings['is_mock'] = True
if entity.startswith("mongomock://"):
conn_settings["is_mock"] = True
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
new_entity = entity.replace("mongomock://", "mongodb://", 1)
resolved_hosts.append(new_entity)
uri_dict = uri_parser.parse_uri(new_entity)
database = uri_dict.get("database")
if database:
conn_settings["name"] = database
# Handle URI style connections, only updating connection params which
# were explicitly specified in the URI.
elif '://' in entity:
elif "://" in entity:
uri_dict = uri_parser.parse_uri(entity)
resolved_hosts.append(entity)
if uri_dict.get('database'):
conn_settings['name'] = uri_dict.get('database')
database = uri_dict.get("database")
if database:
conn_settings["name"] = database
for param in ('read_preference', 'username', 'password'):
for param in ("read_preference", "username", "password"):
if uri_dict.get(param):
conn_settings[param] = uri_dict[param]
uri_options = uri_dict['options']
if 'replicaset' in uri_options:
conn_settings['replicaSet'] = uri_options['replicaset']
if 'authsource' in uri_options:
conn_settings['authentication_source'] = uri_options['authsource']
if 'authmechanism' in uri_options:
conn_settings['authentication_mechanism'] = uri_options['authmechanism']
uri_options = uri_dict["options"]
if "replicaset" in uri_options:
conn_settings["replicaSet"] = uri_options["replicaset"]
if "authsource" in uri_options:
conn_settings["authentication_source"] = uri_options["authsource"]
if "authmechanism" in uri_options:
conn_settings["authentication_mechanism"] = uri_options["authmechanism"]
if "readpreference" in uri_options:
read_preferences = (
ReadPreference.NEAREST,
ReadPreference.PRIMARY,
ReadPreference.PRIMARY_PREFERRED,
ReadPreference.SECONDARY,
ReadPreference.SECONDARY_PREFERRED,
)
# Starting with PyMongo v3.5, the "readpreference" option is
# returned as a string (e.g. "secondaryPreferred") and not an
# int (e.g. 3).
# TODO simplify the code below once we drop support for
# PyMongo v3.4.
read_pf_mode = uri_options["readpreference"]
if isinstance(read_pf_mode, str):
read_pf_mode = read_pf_mode.lower()
for preference in read_preferences:
if (
preference.name.lower() == read_pf_mode
or preference.mode == read_pf_mode
):
conn_settings["read_preference"] = preference
break
else:
resolved_hosts.append(entity)
conn_settings['host'] = resolved_hosts
conn_settings["host"] = resolved_hosts
# Deprecated parameters that should not be passed on
kwargs.pop('slaves', None)
kwargs.pop('is_slave', None)
kwargs.pop("slaves", None)
kwargs.pop("is_slave", None)
conn_settings.update(kwargs)
return conn_settings
def register_connection(
alias,
db=None,
name=None,
host=None,
port=None,
read_preference=READ_PREFERENCE,
username=None,
password=None,
authentication_source=None,
authentication_mechanism=None,
**kwargs,
):
"""Register the connection settings.
:param alias: the name that will be used to refer to this connection throughout MongoEngine
:param db: the name of the database to use, for compatibility with connect
:param name: the name of the specific database to use
:param host: the host name of the: program: `mongod` instance to connect to
:param port: the port that the: program: `mongod` instance is running on
:param read_preference: The read preference for the collection
:param username: username to authenticate with
:param password: password to authenticate with
:param authentication_source: database to authenticate against
:param authentication_mechanism: database authentication mechanisms.
By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
:param is_mock: explicitly use mongomock for this connection
(can also be done by using `mongomock: // ` as db host prefix)
:param kwargs: ad-hoc parameters to be passed into the pymongo driver,
for example maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list.
"""
conn_settings = _get_connection_settings(
db=db,
name=name,
host=host,
port=port,
read_preference=read_preference,
username=username,
password=password,
authentication_source=authentication_source,
authentication_mechanism=authentication_mechanism,
**kwargs,
)
_connection_settings[alias] = conn_settings
def disconnect(alias=DEFAULT_CONNECTION_NAME):
"""Close the connection with a given alias."""
from mongoengine import Document
from mongoengine.base.common import _get_documents_by_db
if alias in _connections:
get_connection(alias=alias).close()
del _connections[alias]
if alias in _dbs:
# Detach all cached collections in Documents
for doc_cls in _get_documents_by_db(alias, DEFAULT_CONNECTION_NAME):
if issubclass(doc_cls, Document): # Skip EmbeddedDocument
doc_cls._disconnect()
del _dbs[alias]
if alias in _connection_settings:
del _connection_settings[alias]
def disconnect_all():
"""Close all registered database."""
for alias in list(_connections.keys()):
disconnect(alias)
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
"""Return a connection with a given alias."""
@ -137,84 +254,93 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
return _connections[alias]
# Validate that the requested alias exists in the _connection_settings.
# Raise MongoEngineConnectionError if it doesn't.
# Raise ConnectionFailure if it doesn't.
if alias not in _connection_settings:
if alias == DEFAULT_CONNECTION_NAME:
msg = 'You have not defined a default connection'
msg = "You have not defined a default connection"
else:
msg = 'Connection with alias "%s" has not been defined' % alias
raise MongoEngineConnectionError(msg)
raise ConnectionFailure(msg)
def _clean_settings(settings_dict):
irrelevant_fields = set([
'name', 'username', 'password', 'authentication_source',
'authentication_mechanism'
])
return {
k: v for k, v in settings_dict.items()
if k not in irrelevant_fields
irrelevant_fields_set = {
"name",
"username",
"password",
"authentication_source",
"authentication_mechanism",
}
return {
k: v for k, v in settings_dict.items() if k not in irrelevant_fields_set
}
raw_conn_settings = _connection_settings[alias].copy()
# Retrieve a copy of the connection settings associated with the requested
# alias and remove the database name and authentication info (we don't
# care about them at this point).
conn_settings = _clean_settings(_connection_settings[alias].copy())
conn_settings = _clean_settings(raw_conn_settings)
# Determine if we should use PyMongo's or mongomock's MongoClient.
is_mock = conn_settings.pop('is_mock', False)
is_mock = conn_settings.pop("is_mock", False)
if is_mock:
try:
import mongomock
except ImportError:
raise RuntimeError('You need mongomock installed to mock '
'MongoEngine.')
raise RuntimeError("You need mongomock installed to mock MongoEngine.")
connection_class = mongomock.MongoClient
else:
connection_class = MongoClient
# For replica set connections with PyMongo 2.x, use
# MongoReplicaSetClient.
# TODO remove this once we stop supporting PyMongo 2.x.
if 'replicaSet' in conn_settings and not IS_PYMONGO_3:
connection_class = MongoReplicaSetClient
conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
# Re-use existing connection if one is suitable.
existing_connection = _find_existing_connection(raw_conn_settings)
if existing_connection:
connection = existing_connection
else:
connection = _create_connection(
alias=alias, connection_class=connection_class, **conn_settings
)
_connections[alias] = connection
return _connections[alias]
# hosts_or_uri has to be a string, so if 'host' was provided
# as a list, join its parts and separate them by ','
if isinstance(conn_settings['hosts_or_uri'], list):
conn_settings['hosts_or_uri'] = ','.join(
conn_settings['hosts_or_uri'])
# Discard port since it can't be used on MongoReplicaSetClient
conn_settings.pop('port', None)
def _create_connection(alias, connection_class, **connection_settings):
"""
Create the new connection for this alias. Raise
ConnectionFailure if it can't be established.
"""
try:
return connection_class(**connection_settings)
except Exception as e:
raise ConnectionFailure(f"Cannot connect to database {alias} :\n{e}")
# Iterate over all of the connection settings and if a connection with
# the same parameters is already established, use it instead of creating
# a new one.
existing_connection = None
connection_settings_iterator = (
def _find_existing_connection(connection_settings):
"""
Check if an existing connection could be reused
Iterate over all of the connection settings and if an existing connection
with the same parameters is suitable, return it
:param connection_settings: the settings of the new connection
:return: An existing connection or None
"""
connection_settings_bis = (
(db_alias, settings.copy())
for db_alias, settings in _connection_settings.items()
)
for db_alias, connection_settings in connection_settings_iterator:
connection_settings = _clean_settings(connection_settings)
if conn_settings == connection_settings and _connections.get(db_alias):
existing_connection = _connections[db_alias]
break
# If an existing connection was found, assign it to the new alias
if existing_connection:
_connections[alias] = existing_connection
else:
# Otherwise, create the new connection for this alias. Raise
# MongoEngineConnectionError if it can't be established.
try:
_connections[alias] = connection_class(**conn_settings)
except Exception as e:
raise MongoEngineConnectionError(
'Cannot connect to database %s :\n%s' % (alias, e))
def _clean_settings(settings_dict):
# Only remove the name but it's important to
# keep the username/password/authentication_source/authentication_mechanism
# to identify if the connection could be shared (cfr https://github.com/MongoEngine/mongoengine/issues/2047)
return {k: v for k, v in settings_dict.items() if k != "name"}
return _connections[alias]
cleaned_conn_settings = _clean_settings(connection_settings)
for db_alias, connection_settings in connection_settings_bis:
db_conn_settings = _clean_settings(connection_settings)
if cleaned_conn_settings == db_conn_settings and _connections.get(db_alias):
return _connections[db_alias]
def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
@ -224,14 +350,18 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
if alias not in _dbs:
conn = get_connection(alias)
conn_settings = _connection_settings[alias]
db = conn[conn_settings['name']]
auth_kwargs = {'source': conn_settings['authentication_source']}
if conn_settings['authentication_mechanism'] is not None:
auth_kwargs['mechanism'] = conn_settings['authentication_mechanism']
db = conn[conn_settings["name"]]
auth_kwargs = {"source": conn_settings["authentication_source"]}
if conn_settings["authentication_mechanism"] is not None:
auth_kwargs["mechanism"] = conn_settings["authentication_mechanism"]
# Authenticate if necessary
if conn_settings['username'] and (conn_settings['password'] or
conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
if conn_settings["username"] and (
conn_settings["password"]
or conn_settings["authentication_mechanism"] == "MONGODB-X509"
):
db.authenticate(
conn_settings["username"], conn_settings["password"], **auth_kwargs
)
_dbs[alias] = db
return _dbs[alias]
@ -244,14 +374,25 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
provide username and password arguments as well.
Multiple databases are supported by using aliases. Provide a separate
`alias` to connect to a different instance of :program:`mongod`.
`alias` to connect to a different instance of: program: `mongod`.
In order to replace a connection identified by a given alias, you'll
need to call ``disconnect`` first
See the docstring for `register_connection` for more details about all
supported kwargs.
.. versionchanged:: 0.6 - added multiple database support.
"""
if alias not in _connections:
if alias in _connections:
prev_conn_setting = _connection_settings[alias]
new_conn_settings = _get_connection_settings(db, **kwargs)
if new_conn_settings != prev_conn_setting:
err_msg = (
"A different connection with alias `{}` was already "
"registered. Use disconnect() first"
).format(alias)
raise ConnectionFailure(err_msg)
else:
register_connection(alias, db, **kwargs)
return get_connection(alias)

View File

@ -1,12 +1,24 @@
from contextlib import contextmanager
from pymongo.read_concern import ReadConcern
from pymongo.write_concern import WriteConcern
from mongoengine.common import _import_class
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
from mongoengine.pymongo_support import count_documents
__all__ = (
"switch_db",
"switch_collection",
"no_dereference",
"no_sub_classes",
"query_counter",
"set_write_concern",
"set_read_write_concern",
)
__all__ = ('switch_db', 'switch_collection', 'no_dereference',
'no_sub_classes', 'query_counter')
class switch_db(object):
class switch_db:
"""switch_db alias context manager.
Example ::
@ -33,21 +45,21 @@ class switch_db(object):
self.cls = cls
self.collection = cls._get_collection()
self.db_alias = db_alias
self.ori_db_alias = cls._meta.get('db_alias', DEFAULT_CONNECTION_NAME)
self.ori_db_alias = cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME)
def __enter__(self):
"""Change the db_alias and clear the cached collection."""
self.cls._meta['db_alias'] = self.db_alias
self.cls._meta["db_alias"] = self.db_alias
self.cls._collection = None
return self.cls
def __exit__(self, t, value, traceback):
"""Reset the db_alias and collection."""
self.cls._meta['db_alias'] = self.ori_db_alias
self.cls._meta["db_alias"] = self.ori_db_alias
self.cls._collection = self.collection
class switch_collection(object):
class switch_collection:
"""switch_collection alias context manager.
Example ::
@ -89,7 +101,7 @@ class switch_collection(object):
self.cls._get_collection_name = self.ori_get_collection_name
class no_dereference(object):
class no_dereference:
"""no_dereference context manager.
Turns off all dereferencing in Documents for the duration of the context
@ -106,14 +118,15 @@ class no_dereference(object):
"""
self.cls = cls
ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class('GenericReferenceField')
ComplexBaseField = _import_class('ComplexBaseField')
ReferenceField = _import_class("ReferenceField")
GenericReferenceField = _import_class("GenericReferenceField")
ComplexBaseField = _import_class("ComplexBaseField")
self.deref_fields = [k for k, v in self.cls._fields.iteritems()
if isinstance(v, (ReferenceField,
GenericReferenceField,
ComplexBaseField))]
self.deref_fields = [
k
for k, v in self.cls._fields.items()
if isinstance(v, (ReferenceField, GenericReferenceField, ComplexBaseField))
]
def __enter__(self):
"""Change the objects default and _auto_dereference values."""
@ -128,7 +141,7 @@ class no_dereference(object):
return self.cls
class no_sub_classes(object):
class no_sub_classes:
"""no_sub_classes context manager.
Only returns instances of this class and no sub (inherited) classes::
@ -143,75 +156,136 @@ class no_sub_classes(object):
:param cls: the class to turn querying sub classes on
"""
self.cls = cls
self.cls_initial_subclasses = None
def __enter__(self):
"""Change the objects default and _auto_dereference values."""
self.cls._all_subclasses = self.cls._subclasses
self.cls._subclasses = (self.cls,)
self.cls_initial_subclasses = self.cls._subclasses
self.cls._subclasses = (self.cls._class_name,)
return self.cls
def __exit__(self, t, value, traceback):
"""Reset the default and _auto_dereference values."""
self.cls._subclasses = self.cls._all_subclasses
delattr(self.cls, '_all_subclasses')
return self.cls
self.cls._subclasses = self.cls_initial_subclasses
class query_counter(object):
"""Query_counter context manager to get the number of queries."""
class query_counter:
"""Query_counter context manager to get the number of queries.
This works by updating the `profiling_level` of the database so that all queries get logged,
resetting the db.system.profile collection at the beginning of the context and counting the new entries.
def __init__(self):
"""Construct the query_counter."""
self.counter = 0
self.db = get_db()
This was designed for debugging purpose. In fact it is a global counter so queries issued by other threads/processes
can interfere with it
def __enter__(self):
"""On every with block we need to drop the profile collection."""
Usage:
.. code-block:: python
class User(Document):
name = StringField()
with query_counter() as q:
user = User(name='Bob')
assert q == 0 # no query fired yet
user.save()
assert q == 1 # 1 query was fired, an 'insert'
user_bis = User.objects().first()
assert q == 2 # a 2nd query was fired, a 'find_one'
Be aware that:
- Iterating over large amount of documents (>101) makes pymongo issue `getmore` queries to fetch the next batch of documents (https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches)
- Some queries are ignored by default by the counter (killcursors, db.system.indexes)
"""
def __init__(self, alias=DEFAULT_CONNECTION_NAME):
self.db = get_db(alias=alias)
self.initial_profiling_level = None
self._ctx_query_counter = 0 # number of queries issued by the context
self._ignored_query = {
"ns": {"$ne": "%s.system.indexes" % self.db.name},
"op": {"$ne": "killcursors"}, # MONGODB < 3.2
"command.killCursors": {"$exists": False}, # MONGODB >= 3.2
}
def _turn_on_profiling(self):
self.initial_profiling_level = self.db.profiling_level()
self.db.set_profiling_level(0)
self.db.system.profile.drop()
self.db.set_profiling_level(2)
def _resets_profiling(self):
self.db.set_profiling_level(self.initial_profiling_level)
def __enter__(self):
self._turn_on_profiling()
return self
def __exit__(self, t, value, traceback):
"""Reset the profiling level."""
self.db.set_profiling_level(0)
self._resets_profiling()
def __eq__(self, value):
"""== Compare querycounter."""
counter = self._get_count()
return value == counter
def __ne__(self, value):
"""!= Compare querycounter."""
return not self.__eq__(value)
def __lt__(self, value):
"""< Compare querycounter."""
return self._get_count() < value
def __le__(self, value):
"""<= Compare querycounter."""
return self._get_count() <= value
def __gt__(self, value):
"""> Compare querycounter."""
return self._get_count() > value
def __ge__(self, value):
""">= Compare querycounter."""
return self._get_count() >= value
def __int__(self):
"""int representation."""
return self._get_count()
def __repr__(self):
"""repr query_counter as the number of queries."""
return u"%s" % self._get_count()
return "%s" % self._get_count()
def _get_count(self):
"""Get the number of queries."""
ignore_query = {'ns': {'$ne': '%s.system.indexes' % self.db.name}}
count = self.db.system.profile.find(ignore_query).count() - self.counter
self.counter += 1
"""Get the number of queries by counting the current number of entries in db.system.profile
and substracting the queries issued by this context. In fact everytime this is called, 1 query is
issued so we need to balance that
"""
count = (
count_documents(self.db.system.profile, self._ignored_query)
- self._ctx_query_counter
)
self._ctx_query_counter += (
1 # Account for the query we just issued to gather the information
)
return count
@contextmanager
def set_write_concern(collection, write_concerns):
combined_concerns = dict(collection.write_concern.document.items())
combined_concerns.update(write_concerns)
yield collection.with_options(write_concern=WriteConcern(**combined_concerns))
@contextmanager
def set_read_write_concern(collection, write_concerns, read_concerns):
combined_write_concerns = dict(collection.write_concern.document.items())
if write_concerns is not None:
combined_write_concerns.update(write_concerns)
combined_read_concerns = dict(collection.read_concern.document.items())
if read_concerns is not None:
combined_read_concerns.update(read_concerns)
yield collection.with_options(
write_concern=WriteConcern(**combined_write_concerns),
read_concern=ReadConcern(**combined_read_concerns),
)

View File

@ -1,16 +1,25 @@
from collections import OrderedDict
from bson import DBRef, SON
import six
from bson import SON, DBRef
from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList,
TopLevelDocumentMetaclass, get_document)
from mongoengine.base import (
BaseDict,
BaseList,
EmbeddedDocumentList,
TopLevelDocumentMetaclass,
get_document,
)
from mongoengine.base.datastructures import LazyReference
from mongoengine.connection import get_db
from mongoengine.document import Document, EmbeddedDocument
from mongoengine.fields import DictField, ListField, MapField, ReferenceField
from mongoengine.fields import (
DictField,
ListField,
MapField,
ReferenceField,
)
from mongoengine.queryset import QuerySet
class DeReference(object):
class DeReference:
def __call__(self, items, max_depth=1, instance=None, name=None):
"""
Cheaply dereferences the items to a set depth.
@ -24,7 +33,7 @@ class DeReference(object):
:class:`~mongoengine.base.ComplexBaseField`
:param get: A boolean determining if being called by __get__
"""
if items is None or isinstance(items, six.string_types):
if items is None or isinstance(items, str):
return items
# cheapest way to convert a queryset to a list
@ -35,43 +44,59 @@ class DeReference(object):
self.max_depth = max_depth
doc_type = None
if instance and isinstance(instance, (Document, EmbeddedDocument,
TopLevelDocumentMetaclass)):
if instance and isinstance(
instance, (Document, EmbeddedDocument, TopLevelDocumentMetaclass)
):
doc_type = instance._fields.get(name)
while hasattr(doc_type, 'field'):
while hasattr(doc_type, "field"):
doc_type = doc_type.field
if isinstance(doc_type, ReferenceField):
field = doc_type
doc_type = doc_type.document_type
is_list = not hasattr(items, 'items')
is_list = not hasattr(items, "items")
if is_list and all([i.__class__ == doc_type for i in items]):
if is_list and all(i.__class__ == doc_type for i in items):
return items
elif not is_list and all(
[i.__class__ == doc_type for i in items.values()]):
i.__class__ == doc_type for i in items.values()
):
return items
elif not field.dbref:
if not hasattr(items, 'items'):
# We must turn the ObjectIds into DBRefs
def _get_items(items):
new_items = []
for v in items:
if isinstance(v, list):
new_items.append(_get_items(v))
elif not isinstance(v, (DBRef, Document)):
new_items.append(field.to_python(v))
else:
new_items.append(v)
return new_items
# Recursively dig into the sub items of a list/dict
# to turn the ObjectIds into DBRefs
def _get_items_from_list(items):
new_items = []
for v in items:
value = v
if isinstance(v, dict):
value = _get_items_from_dict(v)
elif isinstance(v, list):
value = _get_items_from_list(v)
elif not isinstance(v, (DBRef, Document)):
value = field.to_python(v)
new_items.append(value)
return new_items
items = _get_items(items)
def _get_items_from_dict(items):
new_items = {}
for k, v in items.items():
value = v
if isinstance(v, list):
value = _get_items_from_list(v)
elif isinstance(v, dict):
value = _get_items_from_dict(v)
elif not isinstance(v, (DBRef, Document)):
value = field.to_python(v)
new_items[k] = value
return new_items
if not hasattr(items, "items"):
items = _get_items_from_list(items)
else:
items = {
k: (v if isinstance(v, (DBRef, Document))
else field.to_python(v))
for k, v in items.iteritems()
}
items = _get_items_from_dict(items)
self.reference_map = self._find_references(items)
self.object_map = self._fetch_objects(doc_type=doc_type)
@ -98,63 +123,85 @@ class DeReference(object):
depth += 1
for item in iterator:
if isinstance(item, (Document, EmbeddedDocument)):
for field_name, field in item._fields.iteritems():
for field_name, field in item._fields.items():
v = item._data.get(field_name, None)
if isinstance(v, DBRef):
if isinstance(v, LazyReference):
# LazyReference inherits DBRef but should not be dereferenced here !
continue
elif isinstance(v, DBRef):
reference_map.setdefault(field.document_type, set()).add(v.id)
elif isinstance(v, (dict, SON)) and '_ref' in v:
reference_map.setdefault(get_document(v['_cls']), set()).add(v['_ref'].id)
elif isinstance(v, (dict, SON)) and "_ref" in v:
reference_map.setdefault(get_document(v["_cls"]), set()).add(
v["_ref"].id
)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
field_cls = getattr(getattr(field, 'field', None), 'document_type', None)
field_cls = getattr(
getattr(field, "field", None), "document_type", None
)
references = self._find_references(v, depth)
for key, refs in references.iteritems():
if isinstance(field_cls, (Document, TopLevelDocumentMetaclass)):
for key, refs in references.items():
if isinstance(
field_cls, (Document, TopLevelDocumentMetaclass)
):
key = field_cls
reference_map.setdefault(key, set()).update(refs)
elif isinstance(item, LazyReference):
# LazyReference inherits DBRef but should not be dereferenced here !
continue
elif isinstance(item, DBRef):
reference_map.setdefault(item.collection, set()).add(item.id)
elif isinstance(item, (dict, SON)) and '_ref' in item:
reference_map.setdefault(get_document(item['_cls']), set()).add(item['_ref'].id)
elif isinstance(item, (dict, SON)) and "_ref" in item:
reference_map.setdefault(get_document(item["_cls"]), set()).add(
item["_ref"].id
)
elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth:
references = self._find_references(item, depth - 1)
for key, refs in references.iteritems():
for key, refs in references.items():
reference_map.setdefault(key, set()).update(refs)
return reference_map
def _fetch_objects(self, doc_type=None):
"""Fetch all references and convert to their document objects
"""
"""Fetch all references and convert to their document objects"""
object_map = {}
for collection, dbrefs in self.reference_map.iteritems():
if hasattr(collection, 'objects'): # We have a document class for the refs
for collection, dbrefs in self.reference_map.items():
# we use getattr instead of hasattr because hasattr swallows any exception under python2
# so it could hide nasty things without raising exceptions (cfr bug #1688))
ref_document_cls_exists = getattr(collection, "objects", None) is not None
if ref_document_cls_exists:
col_name = collection._get_collection_name()
refs = [dbref for dbref in dbrefs
if (col_name, dbref) not in object_map]
refs = [
dbref for dbref in dbrefs if (col_name, dbref) not in object_map
]
references = collection.objects.in_bulk(refs)
for key, doc in references.iteritems():
for key, doc in references.items():
object_map[(col_name, key)] = doc
else: # Generic reference: use the refs data to convert to document
if isinstance(doc_type, (ListField, DictField, MapField,)):
if isinstance(doc_type, (ListField, DictField, MapField)):
continue
refs = [dbref for dbref in dbrefs
if (collection, dbref) not in object_map]
refs = [
dbref for dbref in dbrefs if (collection, dbref) not in object_map
]
if doc_type:
references = doc_type._get_db()[collection].find({'_id': {'$in': refs}})
references = doc_type._get_db()[collection].find(
{"_id": {"$in": refs}}
)
for ref in references:
doc = doc_type._from_son(ref)
object_map[(collection, doc.id)] = doc
else:
references = get_db()[collection].find({'_id': {'$in': refs}})
references = get_db()[collection].find({"_id": {"$in": refs}})
for ref in references:
if '_cls' in ref:
doc = get_document(ref['_cls'])._from_son(ref)
if "_cls" in ref:
doc = get_document(ref["_cls"])._from_son(ref)
elif doc_type is None:
doc = get_document(
''.join(x.capitalize()
for x in collection.split('_')))._from_son(ref)
"".join(x.capitalize() for x in collection.split("_"))
)._from_son(ref)
else:
doc = doc_type._from_son(ref)
object_map[(collection, doc.id)] = doc
@ -182,19 +229,20 @@ class DeReference(object):
return BaseList(items, instance, name)
if isinstance(items, (dict, SON)):
if '_ref' in items:
if "_ref" in items:
return self.object_map.get(
(items['_ref'].collection, items['_ref'].id), items)
elif '_cls' in items:
doc = get_document(items['_cls'])._from_son(items)
_cls = doc._data.pop('_cls', None)
del items['_cls']
(items["_ref"].collection, items["_ref"].id), items
)
elif "_cls" in items:
doc = get_document(items["_cls"])._from_son(items)
_cls = doc._data.pop("_cls", None)
del items["_cls"]
doc._data = self._attach_objects(doc._data, depth, doc, None)
if _cls is not None:
doc._data['_cls'] = _cls
doc._data["_cls"] = _cls
return doc
if not hasattr(items, 'items'):
if not hasattr(items, "items"):
is_list = True
list_type = BaseList
if isinstance(items, EmbeddedDocumentList):
@ -202,13 +250,9 @@ class DeReference(object):
as_tuple = isinstance(items, tuple)
iterator = enumerate(items)
data = []
elif isinstance(items, OrderedDict):
is_list = False
iterator = items.iteritems()
data = OrderedDict()
else:
is_list = False
iterator = items.iteritems()
iterator = items.items()
data = {}
depth += 1
@ -225,17 +269,23 @@ class DeReference(object):
v = data[k]._data.get(field_name, None)
if isinstance(v, DBRef):
data[k]._data[field_name] = self.object_map.get(
(v.collection, v.id), v)
elif isinstance(v, (dict, SON)) and '_ref' in v:
(v.collection, v.id), v
)
elif isinstance(v, (dict, SON)) and "_ref" in v:
data[k]._data[field_name] = self.object_map.get(
(v['_ref'].collection, v['_ref'].id), v)
(v["_ref"].collection, v["_ref"].id), v
)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
item_name = six.text_type('{0}.{1}.{2}').format(name, k, field_name)
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name)
item_name = f"{name}.{k}.{field_name}"
data[k]._data[field_name] = self._attach_objects(
v, depth, instance=instance, name=item_name
)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
item_name = '%s.%s' % (name, k) if name else name
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name)
elif hasattr(v, 'id'):
item_name = f"{name}.{k}" if name else name
data[k] = self._attach_objects(
v, depth - 1, instance=instance, name=item_name
)
elif isinstance(v, DBRef) and hasattr(v, "id"):
data[k] = self.object_map.get((v.collection, v.id), v)
if instance and name:

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,31 @@
from collections import defaultdict
import six
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
'OperationError', 'NotUniqueError', 'FieldDoesNotExist',
'ValidationError', 'SaveConditionError')
__all__ = (
"NotRegistered",
"InvalidDocumentError",
"LookUpError",
"DoesNotExist",
"MultipleObjectsReturned",
"InvalidQueryError",
"OperationError",
"NotUniqueError",
"BulkWriteError",
"FieldDoesNotExist",
"ValidationError",
"SaveConditionError",
"DeprecatedError",
)
class NotRegistered(Exception):
class MongoEngineException(Exception):
pass
class InvalidDocumentError(Exception):
class NotRegistered(MongoEngineException):
pass
class InvalidDocumentError(MongoEngineException):
pass
@ -20,19 +33,19 @@ class LookUpError(AttributeError):
pass
class DoesNotExist(Exception):
class DoesNotExist(MongoEngineException):
pass
class MultipleObjectsReturned(Exception):
class MultipleObjectsReturned(MongoEngineException):
pass
class InvalidQueryError(Exception):
class InvalidQueryError(MongoEngineException):
pass
class OperationError(Exception):
class OperationError(MongoEngineException):
pass
@ -40,11 +53,15 @@ class NotUniqueError(OperationError):
pass
class BulkWriteError(OperationError):
pass
class SaveConditionError(OperationError):
pass
class FieldDoesNotExist(Exception):
class FieldDoesNotExist(MongoEngineException):
"""Raised when trying to set a field
not declared in a :class:`~mongoengine.Document`
or an :class:`~mongoengine.EmbeddedDocument`.
@ -70,24 +87,25 @@ class ValidationError(AssertionError):
field_name = None
_message = None
def __init__(self, message='', **kwargs):
self.errors = kwargs.get('errors', {})
self.field_name = kwargs.get('field_name')
def __init__(self, message="", **kwargs):
super().__init__(message)
self.errors = kwargs.get("errors", {})
self.field_name = kwargs.get("field_name")
self.message = message
def __str__(self):
return six.text_type(self.message)
return str(self.message)
def __repr__(self):
return '%s(%s,)' % (self.__class__.__name__, self.message)
return f"{self.__class__.__name__}({self.message},)"
def __getattribute__(self, name):
message = super(ValidationError, self).__getattribute__(name)
if name == 'message':
message = super().__getattribute__(name)
if name == "message":
if self.field_name:
message = '%s' % message
message = "%s" % message
if self.errors:
message = '%s(%s)' % (message, self._format_errors())
message = f"{message}({self._format_errors()})"
return message
def _get_message(self):
@ -108,16 +126,13 @@ class ValidationError(AssertionError):
def build_dict(source):
errors_dict = {}
if not source:
return errors_dict
if isinstance(source, dict):
for field_name, error in source.iteritems():
for field_name, error in source.items():
errors_dict[field_name] = build_dict(error)
elif isinstance(source, ValidationError) and source.errors:
return build_dict(source.errors)
else:
return six.text_type(source)
return str(source)
return errors_dict
@ -129,17 +144,22 @@ class ValidationError(AssertionError):
def _format_errors(self):
"""Returns a string listing all errors within a document"""
def generate_key(value, prefix=''):
def generate_key(value, prefix=""):
if isinstance(value, list):
value = ' '.join([generate_key(k) for k in value])
value = " ".join([generate_key(k) for k in value])
elif isinstance(value, dict):
value = ' '.join(
[generate_key(v, k) for k, v in value.iteritems()])
value = " ".join([generate_key(v, k) for k, v in value.items()])
results = '%s.%s' % (prefix, value) if prefix else value
results = f"{prefix}.{value}" if prefix else value
return results
error_dict = defaultdict(list)
for k, v in self.to_dict().iteritems():
for k, v in self.to_dict().items():
error_dict[generate_key(v)].append(k)
return ' '.join(['%s: %s' % (k, v) for k, v in error_dict.iteritems()])
return " ".join([f"{k}: {v}" for k, v in error_dict.items()])
class DeprecatedError(MongoEngineException):
"""Raise when a user uses a feature that has been Deprecated"""
pass

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
"""
Helper functions, constants, and types to aid with MongoDB version support
"""
from mongoengine.connection import get_connection
# Constant that can be used to compare the version retrieved with
# get_mongodb_version()
MONGODB_34 = (3, 4)
MONGODB_36 = (3, 6)
MONGODB_42 = (4, 2)
MONGODB_44 = (4, 4)
def get_mongodb_version():
"""Return the version of the default connected mongoDB (first 2 digits)
:return: tuple(int, int)
"""
version_list = get_connection().server_info()["versionArray"][:2] # e.g: (3, 2)
return tuple(version_list)

View File

@ -0,0 +1,60 @@
"""
Helper functions, constants, and types to aid with PyMongo v2.7 - v3.x support.
"""
import pymongo
from pymongo.errors import OperationFailure
_PYMONGO_37 = (3, 7)
PYMONGO_VERSION = tuple(pymongo.version_tuple[:2])
IS_PYMONGO_GTE_37 = PYMONGO_VERSION >= _PYMONGO_37
def count_documents(
collection, filter, skip=None, limit=None, hint=None, collation=None
):
"""Pymongo>3.7 deprecates count in favour of count_documents"""
if limit == 0:
return 0 # Pymongo raises an OperationFailure if called with limit=0
kwargs = {}
if skip is not None:
kwargs["skip"] = skip
if limit is not None:
kwargs["limit"] = limit
if hint not in (-1, None):
kwargs["hint"] = hint
if collation is not None:
kwargs["collation"] = collation
# count_documents appeared in pymongo 3.7
if IS_PYMONGO_GTE_37:
try:
return collection.count_documents(filter=filter, **kwargs)
except OperationFailure:
# OperationFailure - accounts for some operators that used to work
# with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere)
# fallback to deprecated Cursor.count
# Keeping this should be reevaluated the day pymongo removes .count entirely
pass
cursor = collection.find(filter)
for option, option_value in kwargs.items():
cursor_method = getattr(cursor, option)
cursor = cursor_method(option_value)
with_limit_and_skip = "skip" in kwargs or "limit" in kwargs
return cursor.count(with_limit_and_skip=with_limit_and_skip)
def list_collection_names(db, include_system_collections=False):
"""Pymongo>3.7 deprecates collection_names in favour of list_collection_names"""
if IS_PYMONGO_GTE_37:
collections = db.list_collection_names()
else:
collections = db.collection_names()
if not include_system_collections:
collections = [c for c in collections if not c.startswith("system.")]
return collections

View File

@ -1,25 +0,0 @@
"""
Helper functions, constants, and types to aid with Python v2.7 - v3.x and
PyMongo v2.7 - v3.x support.
"""
import pymongo
import six
if pymongo.version_tuple[0] < 3:
IS_PYMONGO_3 = False
else:
IS_PYMONGO_3 = True
# six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3.
StringIO = six.BytesIO
# Additionally for Py2, try to use the faster cStringIO, if available
if not six.PY3:
try:
import cStringIO
except ImportError:
pass
else:
StringIO = cStringIO.StringIO

View File

@ -7,11 +7,22 @@ from mongoengine.queryset.visitor import *
# Expose just the public subset of all imported objects and constants.
__all__ = (
'QuerySet', 'QuerySetNoCache', 'Q', 'queryset_manager', 'QuerySetManager',
'QueryFieldList', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL',
"QuerySet",
"QuerySetNoCache",
"Q",
"queryset_manager",
"QuerySetManager",
"QueryFieldList",
"DO_NOTHING",
"NULLIFY",
"CASCADE",
"DENY",
"PULL",
# Errors that might be related to a queryset, mostly here for backward
# compatibility
'DoesNotExist', 'InvalidQueryError', 'MultipleObjectsReturned',
'NotUniqueError', 'OperationError',
"DoesNotExist",
"InvalidQueryError",
"MultipleObjectsReturned",
"NotUniqueError",
"OperationError",
)

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,15 @@
__all__ = ('QueryFieldList',)
__all__ = ("QueryFieldList",)
class QueryFieldList(object):
class QueryFieldList:
"""Object that handles combinations of .only() and .exclude() calls"""
ONLY = 1
EXCLUDE = 0
def __init__(self, fields=None, value=ONLY, always_include=None, _only_called=False):
def __init__(
self, fields=None, value=ONLY, always_include=None, _only_called=False
):
"""The QueryFieldList builder
:param fields: A list of fields used in `.only()` or `.exclude()`
@ -49,7 +52,7 @@ class QueryFieldList(object):
self.fields = f.fields - self.fields
self._clean_slice()
if '_id' in f.fields:
if "_id" in f.fields:
self._id = f.value
if self.always_include:
@ -59,11 +62,11 @@ class QueryFieldList(object):
else:
self.fields -= self.always_include
if getattr(f, '_only_called', False):
if getattr(f, "_only_called", False):
self._only_called = True
return self
def __nonzero__(self):
def __bool__(self):
return bool(self.fields)
def as_dict(self):
@ -71,11 +74,11 @@ class QueryFieldList(object):
if self.slice:
field_list.update(self.slice)
if self._id is not None:
field_list['_id'] = self._id
field_list["_id"] = self._id
return field_list
def reset(self):
self.fields = set([])
self.fields = set()
self.slice = {}
self.value = self.ONLY

View File

@ -1,10 +1,11 @@
from functools import partial
from mongoengine.queryset.queryset import QuerySet
__all__ = ('queryset_manager', 'QuerySetManager')
__all__ = ("queryset_manager", "QuerySetManager")
class QuerySetManager(object):
class QuerySetManager:
"""
The default QuerySet Manager.
@ -33,10 +34,10 @@ class QuerySetManager(object):
return self
# owner is the document that contains the QuerySetManager
queryset_class = owner._meta.get('queryset_class', self.default)
queryset_class = owner._meta.get("queryset_class", self.default)
queryset = queryset_class(owner, owner._get_collection())
if self.get_queryset:
arg_count = self.get_queryset.func_code.co_argcount
arg_count = self.get_queryset.__code__.co_argcount
if arg_count == 1:
queryset = self.get_queryset(queryset)
elif arg_count == 2:

View File

@ -1,9 +1,22 @@
from mongoengine.errors import OperationError
from mongoengine.queryset.base import (BaseQuerySet, CASCADE, DENY, DO_NOTHING,
NULLIFY, PULL)
from mongoengine.queryset.base import (
CASCADE,
DENY,
DO_NOTHING,
NULLIFY,
PULL,
BaseQuerySet,
)
__all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE',
'DENY', 'PULL')
__all__ = (
"QuerySet",
"QuerySetNoCache",
"DO_NOTHING",
"NULLIFY",
"CASCADE",
"DENY",
"PULL",
)
# The maximum number of items to display in a QuerySet.__repr__
REPR_OUTPUT_SIZE = 20
@ -55,12 +68,12 @@ class QuerySet(BaseQuerySet):
def __repr__(self):
"""Provide a string representation of the QuerySet"""
if self._iter:
return '.. queryset mid-iteration ..'
return ".. queryset mid-iteration .."
self._populate_cache()
data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
data = self._result_cache[: REPR_OUTPUT_SIZE + 1]
if len(data) > REPR_OUTPUT_SIZE:
data[-1] = '...(remaining elements truncated)...'
data[-1] = "...(remaining elements truncated)..."
return repr(data)
def _iter_results(self):
@ -87,10 +100,10 @@ class QuerySet(BaseQuerySet):
yield self._result_cache[pos]
pos += 1
# Raise StopIteration if we already established there were no more
# return if we already established there were no more
# docs in the db cursor.
if not self._has_more:
raise StopIteration
return
# Otherwise, populate more of the cache and repeat.
if len(self._result_cache) <= pos:
@ -112,8 +125,8 @@ class QuerySet(BaseQuerySet):
# Pull in ITER_CHUNK_SIZE docs from the database and store them in
# the result cache.
try:
for _ in xrange(ITER_CHUNK_SIZE):
self._result_cache.append(self.next())
for _ in range(ITER_CHUNK_SIZE):
self._result_cache.append(next(self))
except StopIteration:
# Getting this exception means there are no more docs in the
# db cursor. Set _has_more to False so that we can use that
@ -128,52 +141,43 @@ class QuerySet(BaseQuerySet):
getting the count
"""
if with_limit_and_skip is False:
return super(QuerySet, self).count(with_limit_and_skip)
return super().count(with_limit_and_skip)
if self._len is None:
self._len = super(QuerySet, self).count(with_limit_and_skip)
# cache the length
self._len = super().count(with_limit_and_skip)
return self._len
def no_cache(self):
"""Convert to a non-caching queryset
.. versionadded:: 0.8.3 Convert to non caching queryset
"""
"""Convert to a non-caching queryset"""
if self._result_cache is not None:
raise OperationError('QuerySet already cached')
raise OperationError("QuerySet already cached")
return self._clone_into(QuerySetNoCache(self._document,
self._collection))
return self._clone_into(QuerySetNoCache(self._document, self._collection))
class QuerySetNoCache(BaseQuerySet):
"""A non caching QuerySet"""
def cache(self):
"""Convert to a caching queryset
.. versionadded:: 0.8.3 Convert to caching queryset
"""
"""Convert to a caching queryset"""
return self._clone_into(QuerySet(self._document, self._collection))
def __repr__(self):
"""Provides the string representation of the QuerySet
.. versionchanged:: 0.6.13 Now doesnt modify the cursor
"""
"""Provides the string representation of the QuerySet"""
if self._iter:
return '.. queryset mid-iteration ..'
return ".. queryset mid-iteration .."
data = []
for _ in xrange(REPR_OUTPUT_SIZE + 1):
for _ in range(REPR_OUTPUT_SIZE + 1):
try:
data.append(self.next())
data.append(next(self))
except StopIteration:
break
if len(data) > REPR_OUTPUT_SIZE:
data[-1] = '...(remaining elements truncated)...'
data[-1] = "...(remaining elements truncated)..."
self.rewind()
return repr(data)
@ -184,10 +188,3 @@ class QuerySetNoCache(BaseQuerySet):
queryset = self.clone()
queryset.rewind()
return queryset
class QuerySetNoDeRef(QuerySet):
"""Special no_dereference QuerySet"""
def __dereference(items, max_depth=1, instance=None, name=None):
return items

View File

@ -1,31 +1,65 @@
from collections import defaultdict
from bson import ObjectId, SON
from bson.dbref import DBRef
import pymongo
import six
from bson import SON, ObjectId
from bson.dbref import DBRef
from mongoengine.base import UPDATE_OPERATORS
from mongoengine.common import _import_class
from mongoengine.connection import get_connection
from mongoengine.errors import InvalidQueryError
from mongoengine.python_support import IS_PYMONGO_3
__all__ = ('query', 'update')
__all__ = ("query", "update", "STRING_OPERATORS")
COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
'all', 'size', 'exists', 'not', 'elemMatch', 'type')
GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
'within_box', 'within_polygon', 'near', 'near_sphere',
'max_distance', 'min_distance', 'geo_within', 'geo_within_box',
'geo_within_polygon', 'geo_within_center',
'geo_within_sphere', 'geo_intersects')
STRING_OPERATORS = ('contains', 'icontains', 'startswith',
'istartswith', 'endswith', 'iendswith',
'exact', 'iexact')
CUSTOM_OPERATORS = ('match',)
MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
STRING_OPERATORS + CUSTOM_OPERATORS)
COMPARISON_OPERATORS = (
"ne",
"gt",
"gte",
"lt",
"lte",
"in",
"nin",
"mod",
"all",
"size",
"exists",
"not",
"elemMatch",
"type",
)
GEO_OPERATORS = (
"within_distance",
"within_spherical_distance",
"within_box",
"within_polygon",
"near",
"near_sphere",
"max_distance",
"min_distance",
"geo_within",
"geo_within_box",
"geo_within_polygon",
"geo_within_center",
"geo_within_sphere",
"geo_intersects",
)
STRING_OPERATORS = (
"contains",
"icontains",
"startswith",
"istartswith",
"endswith",
"iendswith",
"exact",
"iexact",
"regex",
"iregex",
"wholeword",
"iwholeword",
)
CUSTOM_OPERATORS = ("match",)
MATCH_OPERATORS = (
COMPARISON_OPERATORS + GEO_OPERATORS + STRING_OPERATORS + CUSTOM_OPERATORS
)
# TODO make this less complex
@ -34,11 +68,11 @@ def query(_doc_cls=None, **kwargs):
mongo_query = {}
merge_query = defaultdict(list)
for key, value in sorted(kwargs.items()):
if key == '__raw__':
if key == "__raw__":
mongo_query.update(value)
continue
parts = key.rsplit('__')
parts = key.rsplit("__")
indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()]
parts = [part for part in parts if not part.isdigit()]
# Check for an operator and transform to mongo-style if there is
@ -47,11 +81,11 @@ def query(_doc_cls=None, **kwargs):
op = parts.pop()
# Allow to escape operator-like field name by __
if len(parts) > 1 and parts[-1] == '':
if len(parts) > 1 and parts[-1] == "":
parts.pop()
negate = False
if len(parts) > 1 and parts[-1] == 'not':
if len(parts) > 1 and parts[-1] == "not":
parts.pop()
negate = True
@ -63,18 +97,18 @@ def query(_doc_cls=None, **kwargs):
raise InvalidQueryError(e)
parts = []
CachedReferenceField = _import_class('CachedReferenceField')
GenericReferenceField = _import_class('GenericReferenceField')
CachedReferenceField = _import_class("CachedReferenceField")
GenericReferenceField = _import_class("GenericReferenceField")
cleaned_fields = []
for field in fields:
append_field = True
if isinstance(field, six.string_types):
if isinstance(field, str):
parts.append(field)
append_field = False
# is last and CachedReferenceField
elif isinstance(field, CachedReferenceField) and fields[-1] == field:
parts.append('%s._id' % field.db_field)
parts.append("%s._id" % field.db_field)
else:
parts.append(field.db_field)
@ -84,38 +118,17 @@ def query(_doc_cls=None, **kwargs):
# Convert value to proper value
field = cleaned_fields[-1]
singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not']
singular_ops = [None, "ne", "gt", "gte", "lt", "lte", "not"]
singular_ops += STRING_OPERATORS
if op in singular_ops:
if isinstance(field, six.string_types):
if (op in STRING_OPERATORS and
isinstance(value, six.string_types)):
StringField = _import_class('StringField')
value = StringField.prepare_query_value(op, value)
else:
value = field
else:
value = field.prepare_query_value(op, value)
value = field.prepare_query_value(op, value)
if isinstance(field, CachedReferenceField) and value:
value = value['_id']
if isinstance(field, CachedReferenceField) and value:
value = value["_id"]
elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
# Raise an error if the in/nin/all/near param is not iterable. We need a
# special check for BaseDocument, because - although it's iterable - using
# it as such in the context of this method is most definitely a mistake.
BaseDocument = _import_class('BaseDocument')
if isinstance(value, BaseDocument):
raise TypeError("When using the `in`, `nin`, `all`, or "
"`near`-operators you can\'t use a "
"`Document`, you must wrap your object "
"in a list (object -> [object]).")
elif not hasattr(value, '__iter__'):
raise TypeError("The `in`, `nin`, `all`, or "
"`near`-operators must be applied to an "
"iterable (e.g. a list).")
else:
value = [field.prepare_query_value(op, v) for v in value]
elif op in ("in", "nin", "all", "near") and not isinstance(value, dict):
# Raise an error if the in/nin/all/near param is not iterable.
value = _prepare_query_for_iterable(field, op, value)
# If we're querying a GenericReferenceField, we need to alter the
# key depending on the value:
@ -123,73 +136,77 @@ def query(_doc_cls=None, **kwargs):
# * If the value is an ObjectId, the key should be "field_name._ref.$id".
if isinstance(field, GenericReferenceField):
if isinstance(value, DBRef):
parts[-1] += '._ref'
parts[-1] += "._ref"
elif isinstance(value, ObjectId):
parts[-1] += '._ref.$id'
parts[-1] += "._ref.$id"
# if op and op not in COMPARISON_OPERATORS:
if op:
if op in GEO_OPERATORS:
value = _geo_operator(field, op, value)
elif op in ('match', 'elemMatch'):
ListField = _import_class('ListField')
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
elif op in ("match", "elemMatch"):
ListField = _import_class("ListField")
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
if (
isinstance(value, dict) and
isinstance(field, ListField) and
isinstance(field.field, EmbeddedDocumentField)
isinstance(value, dict)
and isinstance(field, ListField)
and isinstance(field.field, EmbeddedDocumentField)
):
value = query(field.field.document_type, **value)
else:
value = field.prepare_query_value(op, value)
value = {'$elemMatch': value}
value = {"$elemMatch": value}
elif op in CUSTOM_OPERATORS:
NotImplementedError('Custom method "%s" has not '
'been implemented' % op)
NotImplementedError(
'Custom method "%s" has not ' "been implemented" % op
)
elif op not in STRING_OPERATORS:
value = {'$' + op: value}
value = {"$" + op: value}
if negate:
value = {'$not': value}
value = {"$not": value}
for i, part in indices:
parts.insert(i, part)
key = '.'.join(parts)
key = ".".join(parts)
if op is None or key not in mongo_query:
if key not in mongo_query:
mongo_query[key] = value
elif key in mongo_query:
if isinstance(mongo_query[key], dict):
else:
if isinstance(mongo_query[key], dict) and isinstance(value, dict):
mongo_query[key].update(value)
# $max/minDistance needs to come last - convert to SON
value_dict = mongo_query[key]
if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and \
('$near' in value_dict or '$nearSphere' in value_dict):
if ("$maxDistance" in value_dict or "$minDistance" in value_dict) and (
"$near" in value_dict or "$nearSphere" in value_dict
):
value_son = SON()
for k, v in value_dict.iteritems():
if k == '$maxDistance' or k == '$minDistance':
for k, v in value_dict.items():
if k == "$maxDistance" or k == "$minDistance":
continue
value_son[k] = v
# Required for MongoDB >= 2.6, may fail when combining
# PyMongo 3+ and MongoDB < 2.6
near_embedded = False
for near_op in ('$near', '$nearSphere'):
if isinstance(value_dict.get(near_op), dict) and (
IS_PYMONGO_3 or get_connection().max_wire_version > 1):
for near_op in ("$near", "$nearSphere"):
if isinstance(value_dict.get(near_op), dict):
value_son[near_op] = SON(value_son[near_op])
if '$maxDistance' in value_dict:
value_son[near_op][
'$maxDistance'] = value_dict['$maxDistance']
if '$minDistance' in value_dict:
value_son[near_op][
'$minDistance'] = value_dict['$minDistance']
if "$maxDistance" in value_dict:
value_son[near_op]["$maxDistance"] = value_dict[
"$maxDistance"
]
if "$minDistance" in value_dict:
value_son[near_op]["$minDistance"] = value_dict[
"$minDistance"
]
near_embedded = True
if not near_embedded:
if '$maxDistance' in value_dict:
value_son['$maxDistance'] = value_dict['$maxDistance']
if '$minDistance' in value_dict:
value_son['$minDistance'] = value_dict['$minDistance']
if "$maxDistance" in value_dict:
value_son["$maxDistance"] = value_dict["$maxDistance"]
if "$minDistance" in value_dict:
value_son["$minDistance"] = value_dict["$minDistance"]
mongo_query[key] = value_son
else:
# Store for manually merging later
@ -201,10 +218,10 @@ def query(_doc_cls=None, **kwargs):
del mongo_query[k]
if isinstance(v, list):
value = [{k: val} for val in v]
if '$and' in mongo_query.keys():
mongo_query['$and'].extend(value)
if "$and" in mongo_query.keys():
mongo_query["$and"].extend(value)
else:
mongo_query['$and'] = value
mongo_query["$and"] = value
return mongo_query
@ -214,37 +231,44 @@ def update(_doc_cls=None, **update):
format.
"""
mongo_update = {}
for key, value in update.items():
if key == '__raw__':
if key == "__raw__":
mongo_update.update(value)
continue
parts = key.split('__')
parts = key.split("__")
# if there is no operator, default to 'set'
if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS:
parts.insert(0, 'set')
parts.insert(0, "set")
# Check for an operator and transform to mongo-style if there is
op = None
if parts[0] in UPDATE_OPERATORS:
op = parts.pop(0)
# Convert Pythonic names to Mongo equivalents
if op in ('push_all', 'pull_all'):
op = op.replace('_all', 'All')
elif op == 'dec':
operator_map = {
"push_all": "pushAll",
"pull_all": "pullAll",
"dec": "inc",
"add_to_set": "addToSet",
"set_on_insert": "setOnInsert",
}
if op == "dec":
# Support decrement by flipping a positive value's sign
# and using 'inc'
op = 'inc'
value = -value
elif op == 'add_to_set':
op = 'addToSet'
elif op == 'set_on_insert':
op = 'setOnInsert'
# If the operator doesn't found from operator map, the op value
# will stay unchanged
op = operator_map.get(op, op)
match = None
if parts[-1] in COMPARISON_OPERATORS:
match = parts.pop()
# Allow to escape operator-like field name by __
if len(parts) > 1 and parts[-1] == '':
if len(parts) > 1 and parts[-1] == "":
parts.pop()
if _doc_cls:
@ -259,10 +283,10 @@ def update(_doc_cls=None, **update):
appended_sub_field = False
for field in fields:
append_field = True
if isinstance(field, six.string_types):
if isinstance(field, str):
# Convert the S operator to $
if field == 'S':
field = '$'
if field == "S":
field = "$"
parts.append(field)
append_field = False
else:
@ -270,7 +294,7 @@ def update(_doc_cls=None, **update):
if append_field:
appended_sub_field = False
cleaned_fields.append(field)
if hasattr(field, 'field'):
if hasattr(field, "field"):
cleaned_fields.append(field.field)
appended_sub_field = True
@ -280,63 +304,91 @@ def update(_doc_cls=None, **update):
else:
field = cleaned_fields[-1]
GeoJsonBaseField = _import_class('GeoJsonBaseField')
GeoJsonBaseField = _import_class("GeoJsonBaseField")
if isinstance(field, GeoJsonBaseField):
value = field.to_mongo(value)
if op in (None, 'set', 'push', 'pull'):
if op == "pull":
if field.required or value is not None:
if match in ("in", "nin") and not isinstance(value, dict):
value = _prepare_query_for_iterable(field, op, value)
else:
value = field.prepare_query_value(op, value)
elif op == "push" and isinstance(value, (list, tuple, set)):
value = [field.prepare_query_value(op, v) for v in value]
elif op in (None, "set", "push"):
if field.required or value is not None:
value = field.prepare_query_value(op, value)
elif op in ('pushAll', 'pullAll'):
elif op in ("pushAll", "pullAll"):
value = [field.prepare_query_value(op, v) for v in value]
elif op in ('addToSet', 'setOnInsert'):
elif op in ("addToSet", "setOnInsert"):
if isinstance(value, (list, tuple, set)):
value = [field.prepare_query_value(op, v) for v in value]
elif field.required or value is not None:
value = field.prepare_query_value(op, value)
elif op == 'unset':
elif op == "unset":
value = 1
elif op == "inc":
value = field.prepare_query_value(op, value)
if match:
match = '$' + match
match = "$" + match
value = {match: value}
key = '.'.join(parts)
key = ".".join(parts)
if not op:
raise InvalidQueryError('Updates must supply an operation '
'eg: set__FIELD=value')
if 'pull' in op and '.' in key:
if "pull" in op and "." in key:
# Dot operators don't work on pull operations
# unless they point to a list field
# Otherwise it uses nested dict syntax
if op == 'pullAll':
raise InvalidQueryError('pullAll operations only support '
'a single field depth')
if op == "pullAll":
raise InvalidQueryError(
"pullAll operations only support a single field depth"
)
# Look for the last list field and use dot notation until there
field_classes = [c.__class__ for c in cleaned_fields]
field_classes.reverse()
ListField = _import_class('ListField')
if ListField in field_classes:
# Join all fields via dot notation to the last ListField
ListField = _import_class("ListField")
EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField")
if ListField in field_classes or EmbeddedDocumentListField in field_classes:
# Join all fields via dot notation to the last ListField or EmbeddedDocumentListField
# Then process as normal
last_listField = len(
cleaned_fields) - field_classes.index(ListField)
key = '.'.join(parts[:last_listField])
if ListField in field_classes:
_check_field = ListField
else:
_check_field = EmbeddedDocumentListField
last_listField = len(cleaned_fields) - field_classes.index(_check_field)
key = ".".join(parts[:last_listField])
parts = parts[last_listField:]
parts.insert(0, key)
parts.reverse()
for key in parts:
value = {key: value}
elif op == 'addToSet' and isinstance(value, list):
value = {key: {'$each': value}}
elif op == "addToSet" and isinstance(value, list):
value = {key: {"$each": value}}
elif op in ("push", "pushAll"):
if parts[-1].isdigit():
key = ".".join(parts[0:-1])
position = int(parts[-1])
# $position expects an iterable. If pushing a single value,
# wrap it in a list.
if not isinstance(value, (set, tuple, list)):
value = [value]
value = {key: {"$each": value, "$position": position}}
else:
if op == "pushAll":
op = "push" # convert to non-deprecated keyword
if not isinstance(value, (set, tuple, list)):
value = [value]
value = {key: {"$each": value}}
else:
value = {key: value}
else:
value = {key: value}
key = '$' + op
key = "$" + op
if key not in mongo_update:
mongo_update[key] = value
elif key in mongo_update and isinstance(mongo_update[key], dict):
@ -347,45 +399,47 @@ def update(_doc_cls=None, **update):
def _geo_operator(field, op, value):
"""Helper to return the query for a given geo query."""
if op == 'max_distance':
value = {'$maxDistance': value}
elif op == 'min_distance':
value = {'$minDistance': value}
if op == "max_distance":
value = {"$maxDistance": value}
elif op == "min_distance":
value = {"$minDistance": value}
elif field._geo_index == pymongo.GEO2D:
if op == 'within_distance':
value = {'$within': {'$center': value}}
elif op == 'within_spherical_distance':
value = {'$within': {'$centerSphere': value}}
elif op == 'within_polygon':
value = {'$within': {'$polygon': value}}
elif op == 'near':
value = {'$near': value}
elif op == 'near_sphere':
value = {'$nearSphere': value}
elif op == 'within_box':
value = {'$within': {'$box': value}}
else:
raise NotImplementedError('Geo method "%s" has not been '
'implemented for a GeoPointField' % op)
else:
if op == 'geo_within':
value = {'$geoWithin': _infer_geometry(value)}
elif op == 'geo_within_box':
value = {'$geoWithin': {'$box': value}}
elif op == 'geo_within_polygon':
value = {'$geoWithin': {'$polygon': value}}
elif op == 'geo_within_center':
value = {'$geoWithin': {'$center': value}}
elif op == 'geo_within_sphere':
value = {'$geoWithin': {'$centerSphere': value}}
elif op == 'geo_intersects':
value = {'$geoIntersects': _infer_geometry(value)}
elif op == 'near':
value = {'$near': _infer_geometry(value)}
if op == "within_distance":
value = {"$within": {"$center": value}}
elif op == "within_spherical_distance":
value = {"$within": {"$centerSphere": value}}
elif op == "within_polygon":
value = {"$within": {"$polygon": value}}
elif op == "near":
value = {"$near": value}
elif op == "near_sphere":
value = {"$nearSphere": value}
elif op == "within_box":
value = {"$within": {"$box": value}}
else:
raise NotImplementedError(
'Geo method "%s" has not been implemented for a %s '
% (op, field._name)
'Geo method "%s" has not been ' "implemented for a GeoPointField" % op
)
else:
if op == "geo_within":
value = {"$geoWithin": _infer_geometry(value)}
elif op == "geo_within_box":
value = {"$geoWithin": {"$box": value}}
elif op == "geo_within_polygon":
value = {"$geoWithin": {"$polygon": value}}
elif op == "geo_within_center":
value = {"$geoWithin": {"$center": value}}
elif op == "geo_within_sphere":
value = {"$geoWithin": {"$centerSphere": value}}
elif op == "geo_intersects":
value = {"$geoIntersects": _infer_geometry(value)}
elif op == "near":
value = {"$near": _infer_geometry(value)}
else:
raise NotImplementedError(
'Geo method "{}" has not been implemented for a {} '.format(
op, field._name
)
)
return value
@ -395,33 +449,58 @@ def _infer_geometry(value):
given value.
"""
if isinstance(value, dict):
if '$geometry' in value:
if "$geometry" in value:
return value
elif 'coordinates' in value and 'type' in value:
return {'$geometry': value}
raise InvalidQueryError('Invalid $geometry dictionary should have '
'type and coordinates keys')
elif "coordinates" in value and "type" in value:
return {"$geometry": value}
raise InvalidQueryError(
"Invalid $geometry dictionary should have type and coordinates keys"
)
elif isinstance(value, (list, set)):
# TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon?
# TODO: should both TypeError and IndexError be alike interpreted?
try:
value[0][0][0]
return {'$geometry': {'type': 'Polygon', 'coordinates': value}}
return {"$geometry": {"type": "Polygon", "coordinates": value}}
except (TypeError, IndexError):
pass
try:
value[0][0]
return {'$geometry': {'type': 'LineString', 'coordinates': value}}
return {"$geometry": {"type": "LineString", "coordinates": value}}
except (TypeError, IndexError):
pass
try:
value[0]
return {'$geometry': {'type': 'Point', 'coordinates': value}}
return {"$geometry": {"type": "Point", "coordinates": value}}
except (TypeError, IndexError):
pass
raise InvalidQueryError('Invalid $geometry data. Can be either a '
'dictionary or (nested) lists of coordinate(s)')
raise InvalidQueryError(
"Invalid $geometry data. Can be either a "
"dictionary or (nested) lists of coordinate(s)"
)
def _prepare_query_for_iterable(field, op, value):
# We need a special check for BaseDocument, because - although it's iterable - using
# it as such in the context of this method is most definitely a mistake.
BaseDocument = _import_class("BaseDocument")
if isinstance(value, BaseDocument):
raise TypeError(
"When using the `in`, `nin`, `all`, or "
"`near`-operators you can't use a "
"`Document`, you must wrap your object "
"in a list (object -> [object])."
)
if not hasattr(value, "__iter__"):
raise TypeError(
"The `in`, `nin`, `all`, or "
"`near`-operators must be applied to an "
"iterable (e.g. a list)."
)
return [field.prepare_query_value(op, v) for v in value]

View File

@ -1,23 +1,26 @@
import copy
import warnings
from mongoengine.errors import InvalidQueryError
from mongoengine.queryset import transform
__all__ = ('Q',)
__all__ = ("Q", "QNode")
class QNodeVisitor(object):
"""Base visitor class for visiting Q-object nodes in a query tree.
"""
def warn_empty_is_deprecated():
msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
warnings.warn(msg, DeprecationWarning, stacklevel=2)
class QNodeVisitor:
"""Base visitor class for visiting Q-object nodes in a query tree."""
def visit_combination(self, combination):
"""Called by QCombination objects.
"""
"""Called by QCombination objects."""
return combination
def visit_query(self, query):
"""Called by (New)Q objects.
"""
"""Called by (New)Q objects."""
return query
@ -43,8 +46,7 @@ class SimplificationVisitor(QNodeVisitor):
return combination
def _query_conjunction(self, queries):
"""Merges query dicts - effectively &ing them together.
"""
"""Merges query dicts - effectively &ing them together."""
query_ops = set()
combined_query = {}
for query in queries:
@ -69,16 +71,16 @@ class QueryCompilerVisitor(QNodeVisitor):
self.document = document
def visit_combination(self, combination):
operator = '$and'
operator = "$and"
if combination.operation == combination.OR:
operator = '$or'
operator = "$or"
return {operator: combination.children}
def visit_query(self, query):
return transform.query(self.document, **query.query)
class QNode(object):
class QNode:
"""Base class for nodes in query trees."""
AND = 0
@ -96,16 +98,19 @@ class QNode(object):
"""Combine this node with another node into a QCombination
object.
"""
if getattr(other, 'empty', True):
# If the other Q() is empty, ignore it and just use `self`.
if not bool(other):
return self
if self.empty:
# Or if this Q is empty, ignore it and just use `other`.
if not bool(self):
return other
return QCombination(operation, [self, other])
@property
def empty(self):
warn_empty_is_deprecated()
return False
def __or__(self, other):
@ -131,6 +136,13 @@ class QCombination(QNode):
else:
self.children.append(node)
def __repr__(self):
op = " & " if self.operation is self.AND else " | "
return "(%s)" % op.join([repr(node) for node in self.children])
def __bool__(self):
return bool(self.children)
def accept(self, visitor):
for i in range(len(self.children)):
if isinstance(self.children[i], QNode):
@ -140,8 +152,16 @@ class QCombination(QNode):
@property
def empty(self):
warn_empty_is_deprecated()
return not bool(self.children)
def __eq__(self, other):
return (
self.__class__ == other.__class__
and self.operation == other.operation
and self.children == other.children
)
class Q(QNode):
"""A simple query object, used in a query tree to build up more complex
@ -151,9 +171,19 @@ class Q(QNode):
def __init__(self, **query):
self.query = query
def __repr__(self):
return "Q(**%s)" % repr(self.query)
def __bool__(self):
return bool(self.query)
def __eq__(self, other):
return self.__class__ == other.__class__ and self.query == other.query
def accept(self, visitor):
return visitor.visit_query(self)
@property
def empty(self):
warn_empty_is_deprecated()
return not bool(self.query)

View File

@ -1,5 +1,12 @@
__all__ = ('pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
'post_save', 'pre_delete', 'post_delete')
__all__ = (
"pre_init",
"post_init",
"pre_save",
"pre_save_post_validation",
"post_save",
"pre_delete",
"post_delete",
)
signals_available = False
try:
@ -7,11 +14,12 @@ try:
signals_available = True
except ImportError:
class Namespace(object):
class Namespace:
def signal(self, name, doc=None):
return _FakeSignal(name, doc)
class _FakeSignal(object):
class _FakeSignal:
"""If blinker is unavailable, create a fake class with the same
interface that allows sending of signals but will fail with an
error on anything else. Instead of doing anything on send, it
@ -23,13 +31,16 @@ except ImportError:
self.__doc__ = doc
def _fail(self, *args, **kwargs):
raise RuntimeError('signalling support is unavailable '
'because the blinker library is '
'not installed.')
raise RuntimeError(
"signalling support is unavailable "
"because the blinker library is "
"not installed."
)
send = lambda *a, **kw: None # noqa
connect = disconnect = has_receivers_for = receivers_for = \
temporarily_connected_to = _fail
connect = (
disconnect
) = has_receivers_for = receivers_for = temporarily_connected_to = _fail
del _fail
@ -37,12 +48,12 @@ except ImportError:
# not put signals in here. Create your own namespace instead.
_signals = Namespace()
pre_init = _signals.signal('pre_init')
post_init = _signals.signal('post_init')
pre_save = _signals.signal('pre_save')
pre_save_post_validation = _signals.signal('pre_save_post_validation')
post_save = _signals.signal('post_save')
pre_delete = _signals.signal('pre_delete')
post_delete = _signals.signal('post_delete')
pre_bulk_insert = _signals.signal('pre_bulk_insert')
post_bulk_insert = _signals.signal('post_bulk_insert')
pre_init = _signals.signal("pre_init")
post_init = _signals.signal("post_init")
pre_save = _signals.signal("pre_save")
pre_save_post_validation = _signals.signal("pre_save_post_validation")
post_save = _signals.signal("post_save")
pre_delete = _signals.signal("pre_delete")
post_delete = _signals.signal("post_delete")
pre_bulk_insert = _signals.signal("pre_bulk_insert")
post_bulk_insert = _signals.signal("post_bulk_insert")

View File

@ -51,4 +51,4 @@ rm -rf $RPM_BUILD_ROOT
# %{python_sitearch}/*
%changelog
* See: http://docs.mongoengine.org/en/latest/changelog.html
* See: http://docs.mongoengine.org/en/latest/changelog.html

7
requirements-dev.txt Normal file
View File

@ -0,0 +1,7 @@
black
flake8
pre-commit
pytest
ipdb
ipython
tox

View File

@ -1,7 +0,0 @@
nose
pymongo>=2.7.1
six==1.10.0
flake8
flake8-import-order
Sphinx==1.5.5
sphinx-rtd-theme==0.2.4

View File

@ -1,11 +1,18 @@
[nosetests]
verbosity=2
detailed-errors=1
tests=tests
cover-package=mongoengine
[flake8]
ignore=E501,F401,F403,F405,I201
ignore=E501,F403,F405,I201,I202,W504,W605,W503,B007
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
max-complexity=47
application-import-names=mongoengine,tests
[tool:pytest]
# Limits the discovery to tests directory
# avoids that it runs for instance the benchmark
testpaths = tests
[isort]
known_first_party = mongoengine,tests
default_section = THIRDPARTY
multi_line_output = 3
include_trailing_comma = True
combine_as_imports = True
line_length = 70
ensure_newline_before_comments = 1

147
setup.py
View File

@ -1,20 +1,20 @@
import os
import sys
from pkg_resources import normalize_path
from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand
# Hack to silence atexit traceback in newer python versions
try:
import multiprocessing
import multiprocessing # noqa: F401
except ImportError:
pass
DESCRIPTION = (
'MongoEngine is a Python Object-Document '
'Mapper for working with MongoDB.'
)
DESCRIPTION = "MongoEngine is a Python Object-Document Mapper for working with MongoDB."
try:
with open('README.rst') as fin:
with open("README.rst") as fin:
LONG_DESCRIPTION = fin.read()
except Exception:
LONG_DESCRIPTION = None
@ -24,64 +24,125 @@ def get_version(version_tuple):
"""Return the version tuple as a string, e.g. for (0, 10, 7),
return '0.10.7'.
"""
return '.'.join(map(str, version_tuple))
return ".".join(map(str, version_tuple))
class PyTest(TestCommand):
"""Will force pytest to search for tests inside the build directory
for 2to3 converted code (used by tox), instead of the current directory.
Required as long as we need 2to3
Known Limitation: https://tox.readthedocs.io/en/latest/example/pytest.html#known-issues-and-limitations
Source: https://www.hackzine.org/python-testing-with-pytest-and-2to3-plus-tox-and-travis-ci.html
"""
# https://pytest.readthedocs.io/en/2.7.3/goodpractises.html#integration-with-setuptools-test-commands
# Allows to provide pytest command argument through the test runner command `python setup.py test`
# e.g: `python setup.py test -a "-k=test"`
# This only works for 1 argument though
user_options = [("pytest-args=", "a", "Arguments to pass to py.test")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ""
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = ["tests"]
self.test_suite = True
def run_tests(self):
# import here, cause outside the eggs aren't loaded
import pytest
from pkg_resources import _namespace_packages
# Purge modules under test from sys.modules. The test loader will
# re-import them from the build location. Required when 2to3 is used
# with namespace packages.
if sys.version_info >= (3,) and getattr(self.distribution, "use_2to3", False):
module = self.test_args[-1].split(".")[0]
if module in _namespace_packages:
del_modules = []
if module in sys.modules:
del_modules.append(module)
module += "."
for name in sys.modules:
if name.startswith(module):
del_modules.append(name)
map(sys.modules.__delitem__, del_modules)
# Run on the build directory for 2to3-built code
# This will prevent the old 2.x code from being found
# by py.test discovery mechanism, that apparently
# ignores sys.path..
ei_cmd = self.get_finalized_command("egg_info")
self.test_args = [normalize_path(ei_cmd.egg_base)]
cmd_args = self.test_args + ([self.pytest_args] if self.pytest_args else [])
errno = pytest.main(cmd_args)
sys.exit(errno)
# Dirty hack to get version number from monogengine/__init__.py - we can't
# import it as it depends on PyMongo and PyMongo isn't installed until this
# file is read
init = os.path.join(os.path.dirname(__file__), 'mongoengine', '__init__.py')
version_line = list(filter(lambda l: l.startswith('VERSION'), open(init)))[0]
init = os.path.join(os.path.dirname(__file__), "mongoengine", "__init__.py")
version_line = list(filter(lambda l: l.startswith("VERSION"), open(init)))[0]
VERSION = get_version(eval(version_line.split('=')[-1]))
VERSION = get_version(eval(version_line.split("=")[-1]))
CLASSIFIERS = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
'Topic :: Database',
'Topic :: Software Development :: Libraries :: Python Modules',
"Topic :: Database",
"Topic :: Software Development :: Libraries :: Python Modules",
]
extra_opts = {
'packages': find_packages(exclude=['tests', 'tests.*']),
'tests_require': ['nose', 'coverage==4.2', 'blinker', 'Pillow>=2.0.0']
"packages": find_packages(exclude=["tests", "tests.*"]),
"tests_require": [
"pytest",
"pytest-cov",
"coverage",
"blinker",
"Pillow>=7.0.0",
],
}
if sys.version_info[0] == 3:
extra_opts['use_2to3'] = True
if 'test' in sys.argv or 'nosetests' in sys.argv:
extra_opts['packages'] = find_packages()
extra_opts['package_data'] = {
'tests': ['fields/mongoengine.png', 'fields/mongodb_leaf.png']}
else:
extra_opts['tests_require'] += ['python-dateutil']
if "test" in sys.argv:
extra_opts["packages"] = find_packages()
extra_opts["package_data"] = {
"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]
}
setup(
name='mongoengine',
name="mongoengine",
version=VERSION,
author='Harry Marr',
author_email='harry.marr@{nospam}gmail.com',
maintainer="Ross Lawley",
maintainer_email="ross.lawley@{nospam}gmail.com",
url='http://mongoengine.org/',
download_url='https://github.com/MongoEngine/mongoengine/tarball/master',
license='MIT',
author="Harry Marr",
author_email="harry.marr@gmail.com",
maintainer="Stefan Wojcik",
maintainer_email="wojcikstefan@gmail.com",
url="http://mongoengine.org/",
download_url="https://github.com/MongoEngine/mongoengine/tarball/master",
license="MIT",
include_package_data=True,
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
platforms=['any'],
platforms=["any"],
classifiers=CLASSIFIERS,
install_requires=['pymongo>=2.7.1', 'six'],
test_suite='nose.collector',
python_requires=">=3.6",
install_requires=["pymongo>=3.4, <4.0"],
cmdclass={"test": PyTest},
**extra_opts
)

View File

@ -1,4 +0,0 @@
from all_warnings import AllWarnings
from document import *
from queryset import *
from fields import *

View File

@ -1,42 +0,0 @@
"""
This test has been put into a module. This is because it tests warnings that
only get triggered on first hit. This way we can ensure its imported into the
top level and called first by the test suite.
"""
import unittest
import warnings
from mongoengine import *
__all__ = ('AllWarnings', )
class AllWarnings(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.warning_list = []
self.showwarning_default = warnings.showwarning
warnings.showwarning = self.append_to_warning_list
def append_to_warning_list(self, message, category, *args):
self.warning_list.append({"message": message,
"category": category})
def tearDown(self):
# restore default handling of warnings
warnings.showwarning = self.showwarning_default
def test_document_collection_syntax_warning(self):
class NonAbstractBase(Document):
meta = {'allow_inheritance': True}
class InheritedDocumentFailTest(NonAbstractBase):
meta = {'collection': 'fail'}
warning = self.warning_list[0]
self.assertEqual(SyntaxWarning, warning["category"])
self.assertEqual('non_abstract_base',
InheritedDocumentFailTest._get_collection_name())

View File

@ -0,0 +1,35 @@
"""
This test has been put into a module. This is because it tests warnings that
only get triggered on first hit. This way we can ensure its imported into the
top level and called first by the test suite.
"""
import unittest
import warnings
from mongoengine import *
class TestAllWarnings(unittest.TestCase):
def setUp(self):
connect(db="mongoenginetest")
self.warning_list = []
self.showwarning_default = warnings.showwarning
warnings.showwarning = self.append_to_warning_list
def append_to_warning_list(self, message, category, *args):
self.warning_list.append({"message": message, "category": category})
def tearDown(self):
# restore default handling of warnings
warnings.showwarning = self.showwarning_default
def test_document_collection_syntax_warning(self):
class NonAbstractBase(Document):
meta = {"allow_inheritance": True}
class InheritedDocumentFailTest(NonAbstractBase):
meta = {"collection": "fail"}
warning = self.warning_list[0]
assert SyntaxWarning == warning["category"]
assert "non_abstract_base" == InheritedDocumentFailTest._get_collection_name()

View File

@ -1,13 +0,0 @@
import unittest
from class_methods import *
from delta import *
from dynamic import *
from indexes import *
from inheritance import *
from instance import *
from json_serialisation import *
from validation import *
if __name__ == '__main__':
unittest.main()

View File

@ -1,350 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from mongoengine import *
from mongoengine.queryset import NULLIFY, PULL
from mongoengine.connection import get_db
__all__ = ("ClassMethodsTest", )
class ClassMethodsTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in self.db.collection_names():
if 'system.' in collection:
continue
self.db.drop_collection(collection)
def test_definition(self):
"""Ensure that document may be defined using fields.
"""
self.assertEqual(['_cls', 'age', 'id', 'name'],
sorted(self.Person._fields.keys()))
self.assertEqual(["IntField", "ObjectIdField", "StringField", "StringField"],
sorted([x.__class__.__name__ for x in
self.Person._fields.values()]))
def test_get_db(self):
"""Ensure that get_db returns the expected db.
"""
db = self.Person._get_db()
self.assertEqual(self.db, db)
def test_get_collection_name(self):
"""Ensure that get_collection_name returns the expected collection
name.
"""
collection_name = 'person'
self.assertEqual(collection_name, self.Person._get_collection_name())
def test_get_collection(self):
"""Ensure that get_collection returns the expected collection.
"""
collection_name = 'person'
collection = self.Person._get_collection()
self.assertEqual(self.db[collection_name], collection)
def test_drop_collection(self):
"""Ensure that the collection may be dropped from the database.
"""
collection_name = 'person'
self.Person(name='Test').save()
self.assertTrue(collection_name in self.db.collection_names())
self.Person.drop_collection()
self.assertFalse(collection_name in self.db.collection_names())
def test_register_delete_rule(self):
"""Ensure that register delete rule adds a delete rule to the document
meta.
"""
class Job(Document):
employee = ReferenceField(self.Person)
self.assertEqual(self.Person._meta.get('delete_rules'), None)
self.Person.register_delete_rule(Job, 'employee', NULLIFY)
self.assertEqual(self.Person._meta['delete_rules'],
{(Job, 'employee'): NULLIFY})
def test_compare_indexes(self):
""" Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
tags = StringField()
meta = {
'indexes': [('author', 'title')]
}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
BlogPost.ensure_index(['author', 'description'])
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [[('author', 1), ('description', 1)]] })
BlogPost._get_collection().drop_index('author_1_description_1')
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
BlogPost._get_collection().drop_index('author_1_title_1')
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [[('author', 1), ('title', 1)]], 'extra': [] })
def test_compare_indexes_inheritance(self):
""" Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes for subclassed
documents (_cls included)
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {
'allow_inheritance': True
}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {
'indexes': [('author', 'tags')]
}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
BlogPostWithTags.ensure_index(['author', 'tag_list'])
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [[('_cls', 1), ('author', 1), ('tag_list', 1)]] })
BlogPostWithTags._get_collection().drop_index('_cls_1_author_1_tag_list_1')
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
BlogPostWithTags._get_collection().drop_index('_cls_1_author_1_tags_1')
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [[('_cls', 1), ('author', 1), ('tags', 1)]], 'extra': [] })
def test_compare_indexes_multiple_subclasses(self):
""" Ensure that compare_indexes behaves correctly if called from a
class, which base class has multiple subclasses
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {
'allow_inheritance': True
}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {
'indexes': [('author', 'tags')]
}
class BlogPostWithCustomField(BlogPost):
custom = DictField()
meta = {
'indexes': [('author', 'custom')]
}
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithCustomField.ensure_indexes()
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
self.assertEqual(BlogPostWithTags.compare_indexes(), { 'missing': [], 'extra': [] })
self.assertEqual(BlogPostWithCustomField.compare_indexes(), { 'missing': [], 'extra': [] })
def test_list_indexes_inheritance(self):
""" ensure that all of the indexes are listed regardless of the super-
or sub-class that we call it from
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {
'allow_inheritance': True
}
class BlogPostWithTags(BlogPost):
tags = StringField()
meta = {
'indexes': [('author', 'tags')]
}
class BlogPostWithTagsAndExtraText(BlogPostWithTags):
extra_text = StringField()
meta = {
'indexes': [('author', 'tags', 'extra_text')]
}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithTagsAndExtraText.ensure_indexes()
self.assertEqual(BlogPost.list_indexes(),
BlogPostWithTags.list_indexes())
self.assertEqual(BlogPost.list_indexes(),
BlogPostWithTagsAndExtraText.list_indexes())
self.assertEqual(BlogPost.list_indexes(),
[[('_cls', 1), ('author', 1), ('tags', 1)],
[('_cls', 1), ('author', 1), ('tags', 1), ('extra_text', 1)],
[(u'_id', 1)], [('_cls', 1)]])
def test_register_delete_rule_inherited(self):
class Vaccine(Document):
name = StringField(required=True)
meta = {"indexes": ["name"]}
class Animal(Document):
family = StringField(required=True)
vaccine_made = ListField(ReferenceField("Vaccine", reverse_delete_rule=PULL))
meta = {"allow_inheritance": True, "indexes": ["family"]}
class Cat(Animal):
name = StringField(required=True)
self.assertEqual(Vaccine._meta['delete_rules'][(Animal, 'vaccine_made')], PULL)
self.assertEqual(Vaccine._meta['delete_rules'][(Cat, 'vaccine_made')], PULL)
def test_collection_naming(self):
"""Ensure that a collection with a specified name may be used.
"""
class DefaultNamingTest(Document):
pass
self.assertEqual('default_naming_test',
DefaultNamingTest._get_collection_name())
class CustomNamingTest(Document):
meta = {'collection': 'pimp_my_collection'}
self.assertEqual('pimp_my_collection',
CustomNamingTest._get_collection_name())
class DynamicNamingTest(Document):
meta = {'collection': lambda c: "DYNAMO"}
self.assertEqual('DYNAMO', DynamicNamingTest._get_collection_name())
# Use Abstract class to handle backwards compatibility
class BaseDocument(Document):
meta = {
'abstract': True,
'collection': lambda c: c.__name__.lower()
}
class OldNamingConvention(BaseDocument):
pass
self.assertEqual('oldnamingconvention',
OldNamingConvention._get_collection_name())
class InheritedAbstractNamingTest(BaseDocument):
meta = {'collection': 'wibble'}
self.assertEqual('wibble',
InheritedAbstractNamingTest._get_collection_name())
# Mixin tests
class BaseMixin(object):
meta = {
'collection': lambda c: c.__name__.lower()
}
class OldMixinNamingConvention(Document, BaseMixin):
pass
self.assertEqual('oldmixinnamingconvention',
OldMixinNamingConvention._get_collection_name())
class BaseMixin(object):
meta = {
'collection': lambda c: c.__name__.lower()
}
class BaseDocument(Document, BaseMixin):
meta = {'allow_inheritance': True}
class MyDocument(BaseDocument):
pass
self.assertEqual('basedocument', MyDocument._get_collection_name())
def test_custom_collection_name_operations(self):
"""Ensure that a collection with a specified name is used as expected.
"""
collection_name = 'personCollTest'
class Person(Document):
name = StringField()
meta = {'collection': collection_name}
Person(name="Test User").save()
self.assertTrue(collection_name in self.db.collection_names())
user_obj = self.db[collection_name].find_one()
self.assertEqual(user_obj['name'], "Test User")
user_obj = Person.objects[0]
self.assertEqual(user_obj.name, "Test User")
Person.drop_collection()
self.assertFalse(collection_name in self.db.collection_names())
def test_collection_name_and_primary(self):
"""Ensure that a collection with a specified name may be used.
"""
class Person(Document):
name = StringField(primary_key=True)
meta = {'collection': 'app'}
Person(name="Test User").save()
user_obj = Person.objects.first()
self.assertEqual(user_obj.name, "Test User")
Person.drop_collection()
if __name__ == '__main__':
unittest.main()

View File

@ -1,867 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from bson import SON
from mongoengine import *
from mongoengine.connection import get_db
__all__ = ("DeltaTest",)
class DeltaTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in self.db.collection_names():
if 'system.' in collection:
continue
self.db.drop_collection(collection)
def test_delta(self):
self.delta(Document)
self.delta(DynamicDocument)
def delta(self, DocClass):
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEqual(doc._get_changed_fields(), ['string_field'])
self.assertEqual(doc._delta(), ({'string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEqual(doc._get_changed_fields(), ['int_field'])
self.assertEqual(doc._delta(), ({'int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({'dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({'list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({}, {'dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({}, {'list_field': 1}))
def test_delta_recursive(self):
self.delta_recursive(Document, EmbeddedDocument)
self.delta_recursive(DynamicDocument, EmbeddedDocument)
self.delta_recursive(Document, DynamicEmbeddedDocument)
self.delta_recursive(DynamicDocument, DynamicEmbeddedDocument)
def delta_recursive(self, DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
id = StringField()
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
embedded_field = EmbeddedDocumentField(Embedded)
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
embedded_1 = Embedded()
embedded_1.id = "010101"
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1
self.assertEqual(doc._get_changed_fields(), ['embedded_field'])
embedded_delta = {
'id': "010101",
'string_field': 'hello',
'int_field': 1,
'dict_field': {'hello': 'world'},
'list_field': ['1', 2, {'hello': 'world'}]
}
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
self.assertEqual(doc._delta(),
({'embedded_field': embedded_delta}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
self.assertEqual(doc._get_changed_fields(),
['embedded_field.dict_field'])
self.assertEqual(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
self.assertEqual(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.dict_field, {})
doc.embedded_field.list_field = []
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field'])
self.assertEqual(doc.embedded_field._delta(), ({}, {'list_field': 1}))
self.assertEqual(doc._delta(), ({}, {'embedded_field.list_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field, [])
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field.list_field = ['1', 2, embedded_2]
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field'])
self.assertEqual(doc.embedded_field._delta(), ({
'list_field': ['1', 2, {
'_cls': 'Embedded',
'string_field': 'hello',
'dict_field': {'hello': 'world'},
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
self.assertEqual(doc._delta(), ({
'embedded_field.list_field': ['1', 2, {
'_cls': 'Embedded',
'string_field': 'hello',
'dict_field': {'hello': 'world'},
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[0], '1')
self.assertEqual(doc.embedded_field.list_field[1], 2)
for k in doc.embedded_field.list_field[2]._fields:
self.assertEqual(doc.embedded_field.list_field[2][k],
embedded_2[k])
doc.embedded_field.list_field[2].string_field = 'world'
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field.2.string_field'])
self.assertEqual(doc.embedded_field._delta(),
({'list_field.2.string_field': 'world'}, {}))
self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.string_field': 'world'}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'world')
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field.2'])
self.assertEqual(doc.embedded_field._delta(), ({'list_field.2': {
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}
}, {}))
self.assertEqual(doc._delta(), ({'embedded_field.list_field.2': {
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'hello world')
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.list_field':
[2, {'hello': 'world'}]}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.list_field':
[2, {'hello': 'world'}, 1]}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[2, {'hello': 'world'}, 1])
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[1, 2, {'hello': 'world'}])
del doc.embedded_field.list_field[2].list_field[2]['hello']
self.assertEqual(doc._delta(),
({}, {'embedded_field.list_field.2.list_field.2.hello': 1}))
doc.save()
doc = doc.reload(10)
del doc.embedded_field.list_field[2].list_field
self.assertEqual(doc._delta(),
({}, {'embedded_field.list_field.2.list_field': 1}))
doc.save()
doc = doc.reload(10)
doc.dict_field['Embedded'] = embedded_1
doc.save()
doc = doc.reload(10)
doc.dict_field['Embedded'].string_field = 'Hello World'
self.assertEqual(doc._get_changed_fields(),
['dict_field.Embedded.string_field'])
self.assertEqual(doc._delta(),
({'dict_field.Embedded.string_field': 'Hello World'}, {}))
def test_circular_reference_deltas(self):
self.circular_reference_deltas(Document, Document)
self.circular_reference_deltas(Document, DynamicDocument)
self.circular_reference_deltas(DynamicDocument, Document)
self.circular_reference_deltas(DynamicDocument, DynamicDocument)
def circular_reference_deltas(self, DocClass1, DocClass2):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField('Organization'))
class Organization(DocClass2):
name = StringField()
owner = ReferenceField('Person')
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
person.save()
organization.save()
p = Person.objects[0].select_related()
o = Organization.objects.first()
self.assertEqual(p.owns[0], o)
self.assertEqual(o.owner, p)
def test_circular_reference_deltas_2(self):
self.circular_reference_deltas_2(Document, Document)
self.circular_reference_deltas_2(Document, DynamicDocument)
self.circular_reference_deltas_2(DynamicDocument, Document)
self.circular_reference_deltas_2(DynamicDocument, DynamicDocument)
def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField('Organization', dbref=dbref))
employer = ReferenceField('Organization', dbref=dbref)
class Organization(DocClass2):
name = StringField()
owner = ReferenceField('Person', dbref=dbref)
employees = ListField(ReferenceField('Person', dbref=dbref))
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
employee = Person(name="employee").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
organization.employees.append(employee)
employee.employer = organization
person.save()
organization.save()
employee.save()
p = Person.objects.get(name="owner")
e = Person.objects.get(name="employee")
o = Organization.objects.first()
self.assertEqual(p.owns[0], o)
self.assertEqual(o.owner, p)
self.assertEqual(e.employer, o)
return person, organization, employee
def test_delta_db_field(self):
self.delta_db_field(Document)
self.delta_db_field(DynamicDocument)
def delta_db_field(self, DocClass):
class Doc(DocClass):
string_field = StringField(db_field='db_string_field')
int_field = IntField(db_field='db_int_field')
dict_field = DictField(db_field='db_dict_field')
list_field = ListField(db_field='db_list_field')
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEqual(doc._get_changed_fields(), ['db_string_field'])
self.assertEqual(doc._delta(), ({'db_string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEqual(doc._get_changed_fields(), ['db_int_field'])
self.assertEqual(doc._delta(), ({'db_int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEqual(doc._get_changed_fields(), ['db_dict_field'])
self.assertEqual(doc._delta(), ({'db_dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEqual(doc._get_changed_fields(), ['db_list_field'])
self.assertEqual(doc._delta(), ({'db_list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEqual(doc._get_changed_fields(), ['db_dict_field'])
self.assertEqual(doc._delta(), ({}, {'db_dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEqual(doc._get_changed_fields(), ['db_list_field'])
self.assertEqual(doc._delta(), ({}, {'db_list_field': 1}))
# Test it saves that data
doc = Doc()
doc.save()
doc.string_field = 'hello'
doc.int_field = 1
doc.dict_field = {'hello': 'world'}
doc.list_field = ['1', 2, {'hello': 'world'}]
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.string_field, 'hello')
self.assertEqual(doc.int_field, 1)
self.assertEqual(doc.dict_field, {'hello': 'world'})
self.assertEqual(doc.list_field, ['1', 2, {'hello': 'world'}])
def test_delta_recursive_db_field(self):
self.delta_recursive_db_field(Document, EmbeddedDocument)
self.delta_recursive_db_field(Document, DynamicEmbeddedDocument)
self.delta_recursive_db_field(DynamicDocument, EmbeddedDocument)
self.delta_recursive_db_field(DynamicDocument, DynamicEmbeddedDocument)
def delta_recursive_db_field(self, DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
string_field = StringField(db_field='db_string_field')
int_field = IntField(db_field='db_int_field')
dict_field = DictField(db_field='db_dict_field')
list_field = ListField(db_field='db_list_field')
class Doc(DocClass):
string_field = StringField(db_field='db_string_field')
int_field = IntField(db_field='db_int_field')
dict_field = DictField(db_field='db_dict_field')
list_field = ListField(db_field='db_list_field')
embedded_field = EmbeddedDocumentField(Embedded,
db_field='db_embedded_field')
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1
self.assertEqual(doc._get_changed_fields(), ['db_embedded_field'])
embedded_delta = {
'db_string_field': 'hello',
'db_int_field': 1,
'db_dict_field': {'hello': 'world'},
'db_list_field': ['1', 2, {'hello': 'world'}]
}
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
self.assertEqual(doc._delta(),
({'db_embedded_field': embedded_delta}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_dict_field'])
self.assertEqual(doc.embedded_field._delta(),
({}, {'db_dict_field': 1}))
self.assertEqual(doc._delta(),
({}, {'db_embedded_field.db_dict_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.dict_field, {})
doc.embedded_field.list_field = []
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field'])
self.assertEqual(doc.embedded_field._delta(),
({}, {'db_list_field': 1}))
self.assertEqual(doc._delta(),
({}, {'db_embedded_field.db_list_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field, [])
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field.list_field = ['1', 2, embedded_2]
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field'])
self.assertEqual(doc.embedded_field._delta(), ({
'db_list_field': ['1', 2, {
'_cls': 'Embedded',
'db_string_field': 'hello',
'db_dict_field': {'hello': 'world'},
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
self.assertEqual(doc._delta(), ({
'db_embedded_field.db_list_field': ['1', 2, {
'_cls': 'Embedded',
'db_string_field': 'hello',
'db_dict_field': {'hello': 'world'},
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[0], '1')
self.assertEqual(doc.embedded_field.list_field[1], 2)
for k in doc.embedded_field.list_field[2]._fields:
self.assertEqual(doc.embedded_field.list_field[2][k],
embedded_2[k])
doc.embedded_field.list_field[2].string_field = 'world'
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field.2.db_string_field'])
self.assertEqual(doc.embedded_field._delta(),
({'db_list_field.2.db_string_field': 'world'}, {}))
self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_string_field': 'world'},
{}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'world')
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field.2'])
self.assertEqual(doc.embedded_field._delta(), ({'db_list_field.2': {
'_cls': 'Embedded',
'db_string_field': 'hello world',
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
'db_dict_field': {'hello': 'world'}}}, {}))
self.assertEqual(doc._delta(), ({
'db_embedded_field.db_list_field.2': {
'_cls': 'Embedded',
'db_string_field': 'hello world',
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
'db_dict_field': {'hello': 'world'}}
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'hello world')
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_list_field':
[2, {'hello': 'world'}]}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_list_field':
[2, {'hello': 'world'}, 1]}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[2, {'hello': 'world'}, 1])
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[1, 2, {'hello': 'world'}])
del doc.embedded_field.list_field[2].list_field[2]['hello']
self.assertEqual(doc._delta(),
({}, {'db_embedded_field.db_list_field.2.db_list_field.2.hello': 1}))
doc.save()
doc = doc.reload(10)
del doc.embedded_field.list_field[2].list_field
self.assertEqual(doc._delta(), ({},
{'db_embedded_field.db_list_field.2.db_list_field': 1}))
def test_delta_for_dynamic_documents(self):
class Person(DynamicDocument):
name = StringField()
meta = {'allow_inheritance': True}
Person.drop_collection()
p = Person(name="James", age=34)
self.assertEqual(p._delta(), (
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
p.doc = 123
del p.doc
self.assertEqual(p._delta(), (
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
p = Person()
p.name = "Dean"
p.age = 22
p.save()
p.age = 24
self.assertEqual(p.age, 24)
self.assertEqual(p._get_changed_fields(), ['age'])
self.assertEqual(p._delta(), ({'age': 24}, {}))
p = Person.objects(age=22).get()
p.age = 24
self.assertEqual(p.age, 24)
self.assertEqual(p._get_changed_fields(), ['age'])
self.assertEqual(p._delta(), ({'age': 24}, {}))
p.save()
self.assertEqual(1, Person.objects(age=24).count())
def test_dynamic_delta(self):
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEqual(doc._get_changed_fields(), ['string_field'])
self.assertEqual(doc._delta(), ({'string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEqual(doc._get_changed_fields(), ['int_field'])
self.assertEqual(doc._delta(), ({'int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({'dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({'list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({}, {'dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({}, {'list_field': 1}))
def test_delta_with_dbref_true(self):
person, organization, employee = self.circular_reference_deltas_2(Document, Document, True)
employee.name = 'test'
self.assertEqual(organization._get_changed_fields(), [])
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertEqual({}, updates)
organization.employees.append(person)
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertTrue('employees' in updates)
def test_delta_with_dbref_false(self):
person, organization, employee = self.circular_reference_deltas_2(Document, Document, False)
employee.name = 'test'
self.assertEqual(organization._get_changed_fields(), [])
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertEqual({}, updates)
organization.employees.append(person)
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertTrue('employees' in updates)
def test_nested_nested_fields_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc)))
name = StringField()
MyDoc.drop_collection()
mydoc = MyDoc(name='testcase1', subs={'a': {'b': EmbeddedDoc(name='foo')}}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs['a']['b']
subdoc.name = 'bar'
self.assertEqual(["name"], subdoc._get_changed_fields())
self.assertEqual(["subs.a.b.name"], mydoc._get_changed_fields())
mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields())
def test_lower_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc().save()
mydoc = MyDoc.objects.first()
mydoc.subs['a'] = EmbeddedDoc()
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
subdoc = mydoc.subs['a']
subdoc.name = 'bar'
self.assertEqual(["name"], subdoc._get_changed_fields())
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
mydoc.save()
mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields())
def test_upper_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc(subs={'a': EmbeddedDoc(name='foo')}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs['a']
subdoc.name = 'bar'
self.assertEqual(["name"], subdoc._get_changed_fields())
self.assertEqual(["subs.a.name"], mydoc._get_changed_fields())
mydoc.subs['a'] = EmbeddedDoc()
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
mydoc.save()
mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields())
def test_referenced_object_changed_attributes(self):
"""Ensures that when you save a new reference to a field, the referenced object isn't altered"""
class Organization(Document):
name = StringField()
class User(Document):
name = StringField()
org = ReferenceField('Organization', required=True)
Organization.drop_collection()
User.drop_collection()
org1 = Organization(name='Org 1')
org1.save()
org2 = Organization(name='Org 2')
org2.save()
user = User(name='Fred', org=org1)
user.save()
org1.reload()
org2.reload()
user.reload()
self.assertEqual(org1.name, 'Org 1')
self.assertEqual(org2.name, 'Org 2')
self.assertEqual(user.name, 'Fred')
user.name = 'Harold'
user.org = org2
org2.name = 'New Org 2'
self.assertEqual(org2.name, 'New Org 2')
user.save()
org2.save()
self.assertEqual(org2.name, 'New Org 2')
org2.reload()
self.assertEqual(org2.name, 'New Org 2')
def test_delta_for_nested_map_fields(self):
class UInfoDocument(Document):
phone = StringField()
class EmbeddedRole(EmbeddedDocument):
type = StringField()
class EmbeddedUser(EmbeddedDocument):
name = StringField()
roles = MapField(field=EmbeddedDocumentField(EmbeddedRole))
rolist = ListField(field=EmbeddedDocumentField(EmbeddedRole))
info = ReferenceField(UInfoDocument)
class Doc(Document):
users = MapField(field=EmbeddedDocumentField(EmbeddedUser))
num = IntField(default=-1)
Doc.drop_collection()
doc = Doc(num=1)
doc.users["007"] = EmbeddedUser(name="Agent007")
doc.save()
uinfo = UInfoDocument(phone="79089269066")
uinfo.save()
d = Doc.objects(num=1).first()
d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin")
d.users["007"]["rolist"].append(EmbeddedRole(type="oops"))
d.users["007"]["info"] = uinfo
delta = d._delta()
self.assertEqual(True, "users.007.roles.666" in delta[0])
self.assertEqual(True, "users.007.rolist" in delta[0])
self.assertEqual(True, "users.007.info" in delta[0])
self.assertEqual('superadmin', delta[0]["users.007.roles.666"]["type"])
self.assertEqual('oops', delta[0]["users.007.rolist"][0]["type"])
self.assertEqual(uinfo.id, delta[0]["users.007.info"])
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,512 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
import warnings
from datetime import datetime
from tests.fixtures import Base
from mongoengine import Document, EmbeddedDocument, connect
from mongoengine.connection import get_db
from mongoengine.fields import (BooleanField, GenericReferenceField,
IntField, StringField)
__all__ = ('InheritanceTest', )
class InheritanceTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def tearDown(self):
for collection in self.db.collection_names():
if 'system.' in collection:
continue
self.db.drop_collection(collection)
def test_superclasses(self):
"""Ensure that the correct list of superclasses is assembled.
"""
class Animal(Document):
meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Fish._superclasses, ('Animal',))
self.assertEqual(Guppy._superclasses, ('Animal', 'Animal.Fish'))
self.assertEqual(Mammal._superclasses, ('Animal',))
self.assertEqual(Dog._superclasses, ('Animal', 'Animal.Mammal'))
self.assertEqual(Human._superclasses, ('Animal', 'Animal.Mammal'))
def test_external_superclasses(self):
"""Ensure that the correct list of super classes is assembled when
importing part of the model.
"""
class Animal(Base): pass
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._superclasses, ('Base', ))
self.assertEqual(Fish._superclasses, ('Base', 'Base.Animal',))
self.assertEqual(Guppy._superclasses, ('Base', 'Base.Animal',
'Base.Animal.Fish'))
self.assertEqual(Mammal._superclasses, ('Base', 'Base.Animal',))
self.assertEqual(Dog._superclasses, ('Base', 'Base.Animal',
'Base.Animal.Mammal'))
self.assertEqual(Human._superclasses, ('Base', 'Base.Animal',
'Base.Animal.Mammal'))
def test_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled.
"""
class Animal(Document):
meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._subclasses, ('Animal',
'Animal.Fish',
'Animal.Fish.Guppy',
'Animal.Mammal',
'Animal.Mammal.Dog',
'Animal.Mammal.Human'))
self.assertEqual(Fish._subclasses, ('Animal.Fish',
'Animal.Fish.Guppy',))
self.assertEqual(Guppy._subclasses, ('Animal.Fish.Guppy',))
self.assertEqual(Mammal._subclasses, ('Animal.Mammal',
'Animal.Mammal.Dog',
'Animal.Mammal.Human'))
self.assertEqual(Human._subclasses, ('Animal.Mammal.Human',))
def test_external_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled when importing part of the model.
"""
class Animal(Base): pass
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._subclasses, ('Base.Animal',
'Base.Animal.Fish',
'Base.Animal.Fish.Guppy',
'Base.Animal.Mammal',
'Base.Animal.Mammal.Dog',
'Base.Animal.Mammal.Human'))
self.assertEqual(Fish._subclasses, ('Base.Animal.Fish',
'Base.Animal.Fish.Guppy',))
self.assertEqual(Guppy._subclasses, ('Base.Animal.Fish.Guppy',))
self.assertEqual(Mammal._subclasses, ('Base.Animal.Mammal',
'Base.Animal.Mammal.Dog',
'Base.Animal.Mammal.Human'))
self.assertEqual(Human._subclasses, ('Base.Animal.Mammal.Human',))
def test_dynamic_declarations(self):
"""Test that declaring an extra class updates meta data"""
class Animal(Document):
meta = {'allow_inheritance': True}
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Animal._subclasses, ('Animal',))
# Test dynamically adding a class changes the meta data
class Fish(Animal):
pass
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish'))
self.assertEqual(Fish._superclasses, ('Animal', ))
self.assertEqual(Fish._subclasses, ('Animal.Fish',))
# Test dynamically adding an inherited class changes the meta data
class Pike(Fish):
pass
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish',
'Animal.Fish.Pike'))
self.assertEqual(Fish._superclasses, ('Animal', ))
self.assertEqual(Fish._subclasses, ('Animal.Fish', 'Animal.Fish.Pike'))
self.assertEqual(Pike._superclasses, ('Animal', 'Animal.Fish'))
self.assertEqual(Pike._subclasses, ('Animal.Fish.Pike',))
def test_inheritance_meta_data(self):
"""Ensure that document may inherit fields from a superclass document.
"""
class Person(Document):
name = StringField()
age = IntField()
meta = {'allow_inheritance': True}
class Employee(Person):
salary = IntField()
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys()))
self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name())
def test_inheritance_to_mongo_keys(self):
"""Ensure that document may inherit fields from a superclass document.
"""
class Person(Document):
name = StringField()
age = IntField()
meta = {'allow_inheritance': True}
class Employee(Person):
salary = IntField()
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys()))
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
['_cls', 'name', 'age'])
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
['_cls', 'name', 'age', 'salary'])
self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name())
def test_indexes_and_multiple_inheritance(self):
""" Ensure that all of the indexes are created for a document with
multiple inheritance.
"""
class A(Document):
a = StringField()
meta = {
'allow_inheritance': True,
'indexes': ['a']
}
class B(Document):
b = StringField()
meta = {
'allow_inheritance': True,
'indexes': ['b']
}
class C(A, B):
pass
A.drop_collection()
B.drop_collection()
C.drop_collection()
C.ensure_indexes()
self.assertEqual(
sorted([idx['key'] for idx in C._get_collection().index_information().values()]),
sorted([[(u'_cls', 1), (u'b', 1)], [(u'_id', 1)], [(u'_cls', 1), (u'a', 1)]])
)
def test_polymorphic_queries(self):
"""Ensure that the correct subclasses are returned from a query
"""
class Animal(Document):
meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
Animal.drop_collection()
Animal().save()
Fish().save()
Mammal().save()
Dog().save()
Human().save()
classes = [obj.__class__ for obj in Animal.objects]
self.assertEqual(classes, [Animal, Fish, Mammal, Dog, Human])
classes = [obj.__class__ for obj in Mammal.objects]
self.assertEqual(classes, [Mammal, Dog, Human])
classes = [obj.__class__ for obj in Human.objects]
self.assertEqual(classes, [Human])
def test_allow_inheritance(self):
"""Ensure that inheritance is disabled by default on simple
classes and that _cls will not be used.
"""
class Animal(Document):
name = StringField()
# can't inherit because Animal didn't explicitly allow inheritance
with self.assertRaises(ValueError):
class Dog(Animal):
pass
# Check that _cls etc aren't present on simple documents
dog = Animal(name='dog').save()
self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
collection = self.db[Animal._get_collection_name()]
obj = collection.find_one()
self.assertFalse('_cls' in obj)
def test_cant_turn_off_inheritance_on_subclass(self):
"""Ensure if inheritance is on in a subclass you cant turn it off.
"""
class Animal(Document):
name = StringField()
meta = {'allow_inheritance': True}
with self.assertRaises(ValueError):
class Mammal(Animal):
meta = {'allow_inheritance': False}
def test_allow_inheritance_abstract_document(self):
"""Ensure that abstract documents can set inheritance rules and that
_cls will not be used.
"""
class FinalDocument(Document):
meta = {'abstract': True,
'allow_inheritance': False}
class Animal(FinalDocument):
name = StringField()
with self.assertRaises(ValueError):
class Mammal(Animal):
pass
# Check that _cls isn't present in simple documents
doc = Animal(name='dog')
self.assertFalse('_cls' in doc.to_mongo())
def test_abstract_handle_ids_in_metaclass_properly(self):
class City(Document):
continent = StringField()
meta = {'abstract': True,
'allow_inheritance': False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name='Berlin', continent='Europe')
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._fields_ordered), 3)
self.assertEqual(berlin._fields_ordered[0], 'id')
def test_auto_id_not_set_if_specific_in_parent_class(self):
class City(Document):
continent = StringField()
city_id = IntField(primary_key=True)
meta = {'abstract': True,
'allow_inheritance': False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name='Berlin', continent='Europe')
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._fields_ordered), 3)
self.assertEqual(berlin._fields_ordered[0], 'city_id')
def test_auto_id_vs_non_pk_id_field(self):
class City(Document):
continent = StringField()
id = IntField()
meta = {'abstract': True,
'allow_inheritance': False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name='Berlin', continent='Europe')
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._fields_ordered), 4)
self.assertEqual(berlin._fields_ordered[0], 'auto_id_0')
berlin.save()
self.assertEqual(berlin.pk, berlin.auto_id_0)
def test_abstract_document_creation_does_not_fail(self):
class City(Document):
continent = StringField()
meta = {'abstract': True,
'allow_inheritance': False}
bkk = City(continent='asia')
self.assertEqual(None, bkk.pk)
# TODO: expected error? Shouldn't we create a new error type?
with self.assertRaises(KeyError):
setattr(bkk, 'pk', 1)
def test_allow_inheritance_embedded_document(self):
"""Ensure embedded documents respect inheritance."""
class Comment(EmbeddedDocument):
content = StringField()
with self.assertRaises(ValueError):
class SpecialComment(Comment):
pass
doc = Comment(content='test')
self.assertFalse('_cls' in doc.to_mongo())
class Comment(EmbeddedDocument):
content = StringField()
meta = {'allow_inheritance': True}
doc = Comment(content='test')
self.assertTrue('_cls' in doc.to_mongo())
def test_document_inheritance(self):
"""Ensure mutliple inheritance of abstract documents
"""
class DateCreatedDocument(Document):
meta = {
'allow_inheritance': True,
'abstract': True,
}
class DateUpdatedDocument(Document):
meta = {
'allow_inheritance': True,
'abstract': True,
}
try:
class MyDocument(DateCreatedDocument, DateUpdatedDocument):
pass
except Exception:
self.assertTrue(False, "Couldn't create MyDocument class")
def test_abstract_documents(self):
"""Ensure that a document superclass can be marked as abstract
thereby not using it as the name for the collection."""
defaults = {'index_background': True,
'index_drop_dups': True,
'index_opts': {'hello': 'world'},
'allow_inheritance': True,
'queryset_class': 'QuerySet',
'db_alias': 'myDB',
'shard_key': ('hello', 'world')}
meta_settings = {'abstract': True}
meta_settings.update(defaults)
class Animal(Document):
name = StringField()
meta = meta_settings
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal):
meta = {'abstract': True}
class Human(Mammal): pass
for k, v in defaults.iteritems():
for cls in [Animal, Fish, Guppy]:
self.assertEqual(cls._meta[k], v)
self.assertFalse('collection' in Animal._meta)
self.assertFalse('collection' in Mammal._meta)
self.assertEqual(Animal._get_collection_name(), None)
self.assertEqual(Mammal._get_collection_name(), None)
self.assertEqual(Fish._get_collection_name(), 'fish')
self.assertEqual(Guppy._get_collection_name(), 'fish')
self.assertEqual(Human._get_collection_name(), 'human')
# ensure that a subclass of a non-abstract class can't be abstract
with self.assertRaises(ValueError):
class EvilHuman(Human):
evil = BooleanField(default=True)
meta = {'abstract': True}
def test_abstract_embedded_documents(self):
# 789: EmbeddedDocument shouldn't inherit abstract
class A(EmbeddedDocument):
meta = {"abstract": True}
class B(A):
pass
self.assertFalse(B._meta["abstract"])
def test_inherited_collections(self):
"""Ensure that subclassed documents don't override parents'
collections
"""
class Drink(Document):
name = StringField()
meta = {'allow_inheritance': True}
class Drinker(Document):
drink = GenericReferenceField()
try:
warnings.simplefilter("error")
class AcloholicDrink(Drink):
meta = {'collection': 'booze'}
except SyntaxWarning:
warnings.simplefilter("ignore")
class AlcoholicDrink(Drink):
meta = {'collection': 'booze'}
else:
raise AssertionError("SyntaxWarning should be triggered")
warnings.resetwarnings()
Drink.drop_collection()
AlcoholicDrink.drop_collection()
Drinker.drop_collection()
red_bull = Drink(name='Red Bull')
red_bull.save()
programmer = Drinker(drink=red_bull)
programmer.save()
beer = AlcoholicDrink(name='Beer')
beer.save()
real_person = Drinker(drink=beer)
real_person.save()
self.assertEqual(Drinker.objects[0].drink.name, red_bull.name)
self.assertEqual(Drinker.objects[1].drink.name, beer.name)
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,349 @@
import unittest
from mongoengine import *
from mongoengine.connection import get_db
from mongoengine.pymongo_support import list_collection_names
from mongoengine.queryset import NULLIFY, PULL
class TestClassMethods(unittest.TestCase):
def setUp(self):
connect(db="mongoenginetest")
self.db = get_db()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_definition(self):
"""Ensure that document may be defined using fields."""
assert ["_cls", "age", "id", "name"] == sorted(self.Person._fields.keys())
assert ["IntField", "ObjectIdField", "StringField", "StringField"] == sorted(
x.__class__.__name__ for x in self.Person._fields.values()
)
def test_get_db(self):
"""Ensure that get_db returns the expected db."""
db = self.Person._get_db()
assert self.db == db
def test_get_collection_name(self):
"""Ensure that get_collection_name returns the expected collection
name.
"""
collection_name = "person"
assert collection_name == self.Person._get_collection_name()
def test_get_collection(self):
"""Ensure that get_collection returns the expected collection."""
collection_name = "person"
collection = self.Person._get_collection()
assert self.db[collection_name] == collection
def test_drop_collection(self):
"""Ensure that the collection may be dropped from the database."""
collection_name = "person"
self.Person(name="Test").save()
assert collection_name in list_collection_names(self.db)
self.Person.drop_collection()
assert collection_name not in list_collection_names(self.db)
def test_register_delete_rule(self):
"""Ensure that register delete rule adds a delete rule to the document
meta.
"""
class Job(Document):
employee = ReferenceField(self.Person)
assert self.Person._meta.get("delete_rules") is None
self.Person.register_delete_rule(Job, "employee", NULLIFY)
assert self.Person._meta["delete_rules"] == {(Job, "employee"): NULLIFY}
def test_compare_indexes(self):
"""Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
tags = StringField()
meta = {"indexes": [("author", "title")]}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPost.ensure_index(["author", "description"])
assert BlogPost.compare_indexes() == {
"missing": [],
"extra": [[("author", 1), ("description", 1)]],
}
BlogPost._get_collection().drop_index("author_1_description_1")
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPost._get_collection().drop_index("author_1_title_1")
assert BlogPost.compare_indexes() == {
"missing": [[("author", 1), ("title", 1)]],
"extra": [],
}
def test_compare_indexes_inheritance(self):
"""Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes for subclassed
documents (_cls included)
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {"allow_inheritance": True}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {"indexes": [("author", "tags")]}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPostWithTags.ensure_index(["author", "tag_list"])
assert BlogPost.compare_indexes() == {
"missing": [],
"extra": [[("_cls", 1), ("author", 1), ("tag_list", 1)]],
}
BlogPostWithTags._get_collection().drop_index("_cls_1_author_1_tag_list_1")
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPostWithTags._get_collection().drop_index("_cls_1_author_1_tags_1")
assert BlogPost.compare_indexes() == {
"missing": [[("_cls", 1), ("author", 1), ("tags", 1)]],
"extra": [],
}
def test_compare_indexes_multiple_subclasses(self):
"""Ensure that compare_indexes behaves correctly if called from a
class, which base class has multiple subclasses
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {"allow_inheritance": True}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {"indexes": [("author", "tags")]}
class BlogPostWithCustomField(BlogPost):
custom = DictField()
meta = {"indexes": [("author", "custom")]}
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithCustomField.ensure_indexes()
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
assert BlogPostWithTags.compare_indexes() == {"missing": [], "extra": []}
assert BlogPostWithCustomField.compare_indexes() == {"missing": [], "extra": []}
def test_compare_indexes_for_text_indexes(self):
"""Ensure that compare_indexes behaves correctly for text indexes"""
class Doc(Document):
a = StringField()
b = StringField()
meta = {
"indexes": [
{
"fields": ["$a", "$b"],
"default_language": "english",
"weights": {"a": 10, "b": 2},
}
]
}
Doc.drop_collection()
Doc.ensure_indexes()
actual = Doc.compare_indexes()
expected = {"missing": [], "extra": []}
assert actual == expected
def test_list_indexes_inheritance(self):
"""ensure that all of the indexes are listed regardless of the super-
or sub-class that we call it from
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {"allow_inheritance": True}
class BlogPostWithTags(BlogPost):
tags = StringField()
meta = {"indexes": [("author", "tags")]}
class BlogPostWithTagsAndExtraText(BlogPostWithTags):
extra_text = StringField()
meta = {"indexes": [("author", "tags", "extra_text")]}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithTagsAndExtraText.ensure_indexes()
assert BlogPost.list_indexes() == BlogPostWithTags.list_indexes()
assert BlogPost.list_indexes() == BlogPostWithTagsAndExtraText.list_indexes()
assert BlogPost.list_indexes() == [
[("_cls", 1), ("author", 1), ("tags", 1)],
[("_cls", 1), ("author", 1), ("tags", 1), ("extra_text", 1)],
[("_id", 1)],
[("_cls", 1)],
]
def test_register_delete_rule_inherited(self):
class Vaccine(Document):
name = StringField(required=True)
meta = {"indexes": ["name"]}
class Animal(Document):
family = StringField(required=True)
vaccine_made = ListField(
ReferenceField("Vaccine", reverse_delete_rule=PULL)
)
meta = {"allow_inheritance": True, "indexes": ["family"]}
class Cat(Animal):
name = StringField(required=True)
assert Vaccine._meta["delete_rules"][(Animal, "vaccine_made")] == PULL
assert Vaccine._meta["delete_rules"][(Cat, "vaccine_made")] == PULL
def test_collection_naming(self):
"""Ensure that a collection with a specified name may be used."""
class DefaultNamingTest(Document):
pass
assert "default_naming_test" == DefaultNamingTest._get_collection_name()
class CustomNamingTest(Document):
meta = {"collection": "pimp_my_collection"}
assert "pimp_my_collection" == CustomNamingTest._get_collection_name()
class DynamicNamingTest(Document):
meta = {"collection": lambda c: "DYNAMO"}
assert "DYNAMO" == DynamicNamingTest._get_collection_name()
# Use Abstract class to handle backwards compatibility
class BaseDocument(Document):
meta = {"abstract": True, "collection": lambda c: c.__name__.lower()}
class OldNamingConvention(BaseDocument):
pass
assert "oldnamingconvention" == OldNamingConvention._get_collection_name()
class InheritedAbstractNamingTest(BaseDocument):
meta = {"collection": "wibble"}
assert "wibble" == InheritedAbstractNamingTest._get_collection_name()
# Mixin tests
class BaseMixin:
meta = {"collection": lambda c: c.__name__.lower()}
class OldMixinNamingConvention(Document, BaseMixin):
pass
assert (
"oldmixinnamingconvention"
== OldMixinNamingConvention._get_collection_name()
)
class BaseMixin:
meta = {"collection": lambda c: c.__name__.lower()}
class BaseDocument(Document, BaseMixin):
meta = {"allow_inheritance": True}
class MyDocument(BaseDocument):
pass
assert "basedocument" == MyDocument._get_collection_name()
def test_custom_collection_name_operations(self):
"""Ensure that a collection with a specified name is used as expected."""
collection_name = "personCollTest"
class Person(Document):
name = StringField()
meta = {"collection": collection_name}
Person(name="Test User").save()
assert collection_name in list_collection_names(self.db)
user_obj = self.db[collection_name].find_one()
assert user_obj["name"] == "Test User"
user_obj = Person.objects[0]
assert user_obj.name == "Test User"
Person.drop_collection()
assert collection_name not in list_collection_names(self.db)
def test_collection_name_and_primary(self):
"""Ensure that a collection with a specified name may be used."""
class Person(Document):
name = StringField(primary_key=True)
meta = {"collection": "app"}
Person(name="Test User").save()
user_obj = Person.objects.first()
assert user_obj.name == "Test User"
Person.drop_collection()
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,957 @@
import unittest
from bson import SON
from mongoengine import *
from mongoengine.pymongo_support import list_collection_names
from tests.utils import MongoDBTestCase
class TestDelta(MongoDBTestCase):
def setUp(self):
super().setUp()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_delta(self):
self.delta(Document)
self.delta(DynamicDocument)
@staticmethod
def delta(DocClass):
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
doc.string_field = "hello"
assert doc._get_changed_fields() == ["string_field"]
assert doc._delta() == ({"string_field": "hello"}, {})
doc._changed_fields = []
doc.int_field = 1
assert doc._get_changed_fields() == ["int_field"]
assert doc._delta() == ({"int_field": 1}, {})
doc._changed_fields = []
dict_value = {"hello": "world", "ping": "pong"}
doc.dict_field = dict_value
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({"dict_field": dict_value}, {})
doc._changed_fields = []
list_value = ["1", 2, {"hello": "world"}]
doc.list_field = list_value
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({"list_field": list_value}, {})
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({}, {"dict_field": 1})
doc._changed_fields = []
doc.list_field = []
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({}, {"list_field": 1})
def test_delta_recursive(self):
self.delta_recursive(Document, EmbeddedDocument)
self.delta_recursive(DynamicDocument, EmbeddedDocument)
self.delta_recursive(Document, DynamicEmbeddedDocument)
self.delta_recursive(DynamicDocument, DynamicEmbeddedDocument)
def delta_recursive(self, DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
id = StringField()
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
embedded_field = EmbeddedDocumentField(Embedded)
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
embedded_1 = Embedded()
embedded_1.id = "010101"
embedded_1.string_field = "hello"
embedded_1.int_field = 1
embedded_1.dict_field = {"hello": "world"}
embedded_1.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field = embedded_1
assert doc._get_changed_fields() == ["embedded_field"]
embedded_delta = {
"id": "010101",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ["1", 2, {"hello": "world"}],
}
assert doc.embedded_field._delta() == (embedded_delta, {})
assert doc._delta() == ({"embedded_field": embedded_delta}, {})
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
assert doc._get_changed_fields() == ["embedded_field.dict_field"]
assert doc.embedded_field._delta() == ({}, {"dict_field": 1})
assert doc._delta() == ({}, {"embedded_field.dict_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.dict_field == {}
doc.embedded_field.list_field = []
assert doc._get_changed_fields() == ["embedded_field.list_field"]
assert doc.embedded_field._delta() == ({}, {"list_field": 1})
assert doc._delta() == ({}, {"embedded_field.list_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field == []
embedded_2 = Embedded()
embedded_2.string_field = "hello"
embedded_2.int_field = 1
embedded_2.dict_field = {"hello": "world"}
embedded_2.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field.list_field = ["1", 2, embedded_2]
assert doc._get_changed_fields() == ["embedded_field.list_field"]
assert doc.embedded_field._delta() == (
{
"list_field": [
"1",
2,
{
"_cls": "Embedded",
"string_field": "hello",
"dict_field": {"hello": "world"},
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
assert doc._delta() == (
{
"embedded_field.list_field": [
"1",
2,
{
"_cls": "Embedded",
"string_field": "hello",
"dict_field": {"hello": "world"},
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[0] == "1"
assert doc.embedded_field.list_field[1] == 2
for k in doc.embedded_field.list_field[2]._fields:
assert doc.embedded_field.list_field[2][k] == embedded_2[k]
doc.embedded_field.list_field[2].string_field = "world"
assert doc._get_changed_fields() == ["embedded_field.list_field.2.string_field"]
assert doc.embedded_field._delta() == (
{"list_field.2.string_field": "world"},
{},
)
assert doc._delta() == (
{"embedded_field.list_field.2.string_field": "world"},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "world"
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = "hello world"
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
assert doc._get_changed_fields() == ["embedded_field.list_field.2"]
assert doc.embedded_field._delta() == (
{
"list_field.2": {
"_cls": "Embedded",
"string_field": "hello world",
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
"dict_field": {"hello": "world"},
}
},
{},
)
assert doc._delta() == (
{
"embedded_field.list_field.2": {
"_cls": "Embedded",
"string_field": "hello world",
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
"dict_field": {"hello": "world"},
}
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "hello world"
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
assert doc._delta() == (
{"embedded_field.list_field.2.list_field": [2, {"hello": "world"}]},
{},
)
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
assert doc._delta() == (
{"embedded_field.list_field.2.list_field": [2, {"hello": "world"}, 1]},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [2, {"hello": "world"}, 1]
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [1, 2, {"hello": "world"}]
del doc.embedded_field.list_field[2].list_field[2]["hello"]
assert doc._delta() == (
{},
{"embedded_field.list_field.2.list_field.2.hello": 1},
)
doc.save()
doc = doc.reload(10)
del doc.embedded_field.list_field[2].list_field
assert doc._delta() == ({}, {"embedded_field.list_field.2.list_field": 1})
doc.save()
doc = doc.reload(10)
doc.dict_field["Embedded"] = embedded_1
doc.save()
doc = doc.reload(10)
doc.dict_field["Embedded"].string_field = "Hello World"
assert doc._get_changed_fields() == ["dict_field.Embedded.string_field"]
assert doc._delta() == ({"dict_field.Embedded.string_field": "Hello World"}, {})
def test_circular_reference_deltas(self):
self.circular_reference_deltas(Document, Document)
self.circular_reference_deltas(Document, DynamicDocument)
self.circular_reference_deltas(DynamicDocument, Document)
self.circular_reference_deltas(DynamicDocument, DynamicDocument)
def circular_reference_deltas(self, DocClass1, DocClass2):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField("Organization"))
class Organization(DocClass2):
name = StringField()
owner = ReferenceField("Person")
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
person.save()
organization.save()
p = Person.objects[0].select_related()
o = Organization.objects.first()
assert p.owns[0] == o
assert o.owner == p
def test_circular_reference_deltas_2(self):
self.circular_reference_deltas_2(Document, Document)
self.circular_reference_deltas_2(Document, DynamicDocument)
self.circular_reference_deltas_2(DynamicDocument, Document)
self.circular_reference_deltas_2(DynamicDocument, DynamicDocument)
def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField("Organization", dbref=dbref))
employer = ReferenceField("Organization", dbref=dbref)
class Organization(DocClass2):
name = StringField()
owner = ReferenceField("Person", dbref=dbref)
employees = ListField(ReferenceField("Person", dbref=dbref))
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
employee = Person(name="employee").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
organization.employees.append(employee)
employee.employer = organization
person.save()
organization.save()
employee.save()
p = Person.objects.get(name="owner")
e = Person.objects.get(name="employee")
o = Organization.objects.first()
assert p.owns[0] == o
assert o.owner == p
assert e.employer == o
return person, organization, employee
def test_delta_db_field(self):
self.delta_db_field(Document)
self.delta_db_field(DynamicDocument)
def delta_db_field(self, DocClass):
class Doc(DocClass):
string_field = StringField(db_field="db_string_field")
int_field = IntField(db_field="db_int_field")
dict_field = DictField(db_field="db_dict_field")
list_field = ListField(db_field="db_list_field")
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
doc.string_field = "hello"
assert doc._get_changed_fields() == ["db_string_field"]
assert doc._delta() == ({"db_string_field": "hello"}, {})
doc._changed_fields = []
doc.int_field = 1
assert doc._get_changed_fields() == ["db_int_field"]
assert doc._delta() == ({"db_int_field": 1}, {})
doc._changed_fields = []
dict_value = {"hello": "world", "ping": "pong"}
doc.dict_field = dict_value
assert doc._get_changed_fields() == ["db_dict_field"]
assert doc._delta() == ({"db_dict_field": dict_value}, {})
doc._changed_fields = []
list_value = ["1", 2, {"hello": "world"}]
doc.list_field = list_value
assert doc._get_changed_fields() == ["db_list_field"]
assert doc._delta() == ({"db_list_field": list_value}, {})
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
assert doc._get_changed_fields() == ["db_dict_field"]
assert doc._delta() == ({}, {"db_dict_field": 1})
doc._changed_fields = []
doc.list_field = []
assert doc._get_changed_fields() == ["db_list_field"]
assert doc._delta() == ({}, {"db_list_field": 1})
# Test it saves that data
doc = Doc()
doc.save()
doc.string_field = "hello"
doc.int_field = 1
doc.dict_field = {"hello": "world"}
doc.list_field = ["1", 2, {"hello": "world"}]
doc.save()
doc = doc.reload(10)
assert doc.string_field == "hello"
assert doc.int_field == 1
assert doc.dict_field == {"hello": "world"}
assert doc.list_field == ["1", 2, {"hello": "world"}]
def test_delta_recursive_db_field_on_doc_and_embeddeddoc(self):
self.delta_recursive_db_field(Document, EmbeddedDocument)
def test_delta_recursive_db_field_on_doc_and_dynamicembeddeddoc(self):
self.delta_recursive_db_field(Document, DynamicEmbeddedDocument)
def test_delta_recursive_db_field_on_dynamicdoc_and_embeddeddoc(self):
self.delta_recursive_db_field(DynamicDocument, EmbeddedDocument)
def test_delta_recursive_db_field_on_dynamicdoc_and_dynamicembeddeddoc(self):
self.delta_recursive_db_field(DynamicDocument, DynamicEmbeddedDocument)
@staticmethod
def delta_recursive_db_field(DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
string_field = StringField(db_field="db_string_field")
int_field = IntField(db_field="db_int_field")
dict_field = DictField(db_field="db_dict_field")
list_field = ListField(db_field="db_list_field")
class Doc(DocClass):
string_field = StringField(db_field="db_string_field")
int_field = IntField(db_field="db_int_field")
dict_field = DictField(db_field="db_dict_field")
list_field = ListField(db_field="db_list_field")
embedded_field = EmbeddedDocumentField(
Embedded, db_field="db_embedded_field"
)
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
embedded_1 = Embedded()
embedded_1.string_field = "hello"
embedded_1.int_field = 1
embedded_1.dict_field = {"hello": "world"}
embedded_1.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field = embedded_1
assert doc._get_changed_fields() == ["db_embedded_field"]
embedded_delta = {
"db_string_field": "hello",
"db_int_field": 1,
"db_dict_field": {"hello": "world"},
"db_list_field": ["1", 2, {"hello": "world"}],
}
assert doc.embedded_field._delta() == (embedded_delta, {})
assert doc._delta() == ({"db_embedded_field": embedded_delta}, {})
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
assert doc._get_changed_fields() == ["db_embedded_field.db_dict_field"]
assert doc.embedded_field._delta() == ({}, {"db_dict_field": 1})
assert doc._delta() == ({}, {"db_embedded_field.db_dict_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.dict_field == {}
assert doc._get_changed_fields() == []
doc.embedded_field.list_field = []
assert doc._get_changed_fields() == ["db_embedded_field.db_list_field"]
assert doc.embedded_field._delta() == ({}, {"db_list_field": 1})
assert doc._delta() == ({}, {"db_embedded_field.db_list_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field == []
embedded_2 = Embedded()
embedded_2.string_field = "hello"
embedded_2.int_field = 1
embedded_2.dict_field = {"hello": "world"}
embedded_2.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field.list_field = ["1", 2, embedded_2]
assert doc._get_changed_fields() == ["db_embedded_field.db_list_field"]
assert doc.embedded_field._delta() == (
{
"db_list_field": [
"1",
2,
{
"_cls": "Embedded",
"db_string_field": "hello",
"db_dict_field": {"hello": "world"},
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
assert doc._delta() == (
{
"db_embedded_field.db_list_field": [
"1",
2,
{
"_cls": "Embedded",
"db_string_field": "hello",
"db_dict_field": {"hello": "world"},
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
doc.save()
assert doc._get_changed_fields() == []
doc = doc.reload(10)
assert doc.embedded_field.list_field[0] == "1"
assert doc.embedded_field.list_field[1] == 2
for k in doc.embedded_field.list_field[2]._fields:
assert doc.embedded_field.list_field[2][k] == embedded_2[k]
doc.embedded_field.list_field[2].string_field = "world"
assert doc._get_changed_fields() == [
"db_embedded_field.db_list_field.2.db_string_field"
]
assert doc.embedded_field._delta() == (
{"db_list_field.2.db_string_field": "world"},
{},
)
assert doc._delta() == (
{"db_embedded_field.db_list_field.2.db_string_field": "world"},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "world"
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = "hello world"
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
assert doc._get_changed_fields() == ["db_embedded_field.db_list_field.2"]
assert doc.embedded_field._delta() == (
{
"db_list_field.2": {
"_cls": "Embedded",
"db_string_field": "hello world",
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
"db_dict_field": {"hello": "world"},
}
},
{},
)
assert doc._delta() == (
{
"db_embedded_field.db_list_field.2": {
"_cls": "Embedded",
"db_string_field": "hello world",
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
"db_dict_field": {"hello": "world"},
}
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "hello world"
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
assert doc._delta() == (
{
"db_embedded_field.db_list_field.2.db_list_field": [
2,
{"hello": "world"},
]
},
{},
)
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
assert doc._delta() == (
{
"db_embedded_field.db_list_field.2.db_list_field": [
2,
{"hello": "world"},
1,
]
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [2, {"hello": "world"}, 1]
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [1, 2, {"hello": "world"}]
del doc.embedded_field.list_field[2].list_field[2]["hello"]
assert doc._delta() == (
{},
{"db_embedded_field.db_list_field.2.db_list_field.2.hello": 1},
)
doc.save()
doc = doc.reload(10)
assert doc._delta() == (
{},
{},
)
del doc.embedded_field.list_field[2].list_field
assert doc._delta() == (
{},
{"db_embedded_field.db_list_field.2.db_list_field": 1},
)
def test_delta_for_dynamic_documents(self):
class Person(DynamicDocument):
name = StringField()
meta = {"allow_inheritance": True}
Person.drop_collection()
p = Person(name="James", age=34)
assert p._delta() == (
SON([("_cls", "Person"), ("name", "James"), ("age", 34)]),
{},
)
p.doc = 123
del p.doc
assert p._delta() == (
SON([("_cls", "Person"), ("name", "James"), ("age", 34)]),
{},
)
p = Person()
p.name = "Dean"
p.age = 22
p.save()
p.age = 24
assert p.age == 24
assert p._get_changed_fields() == ["age"]
assert p._delta() == ({"age": 24}, {})
p = Person.objects(age=22).get()
p.age = 24
assert p.age == 24
assert p._get_changed_fields() == ["age"]
assert p._delta() == ({"age": 24}, {})
p.save()
assert 1 == Person.objects(age=24).count()
def test_dynamic_delta(self):
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
doc.string_field = "hello"
assert doc._get_changed_fields() == ["string_field"]
assert doc._delta() == ({"string_field": "hello"}, {})
doc._changed_fields = []
doc.int_field = 1
assert doc._get_changed_fields() == ["int_field"]
assert doc._delta() == ({"int_field": 1}, {})
doc._changed_fields = []
dict_value = {"hello": "world", "ping": "pong"}
doc.dict_field = dict_value
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({"dict_field": dict_value}, {})
doc._changed_fields = []
list_value = ["1", 2, {"hello": "world"}]
doc.list_field = list_value
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({"list_field": list_value}, {})
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({}, {"dict_field": 1})
doc._changed_fields = []
doc.list_field = []
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({}, {"list_field": 1})
def test_delta_with_dbref_true(self):
person, organization, employee = self.circular_reference_deltas_2(
Document, Document, True
)
employee.name = "test"
assert organization._get_changed_fields() == []
updates, removals = organization._delta()
assert removals == {}
assert updates == {}
organization.employees.append(person)
updates, removals = organization._delta()
assert removals == {}
assert "employees" in updates
def test_delta_with_dbref_false(self):
person, organization, employee = self.circular_reference_deltas_2(
Document, Document, False
)
employee.name = "test"
assert organization._get_changed_fields() == []
updates, removals = organization._delta()
assert removals == {}
assert updates == {}
organization.employees.append(person)
updates, removals = organization._delta()
assert removals == {}
assert "employees" in updates
def test_nested_nested_fields_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc)))
name = StringField()
MyDoc.drop_collection()
MyDoc(name="testcase1", subs={"a": {"b": EmbeddedDoc(name="foo")}}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs["a"]["b"]
subdoc.name = "bar"
assert subdoc._get_changed_fields() == ["name"]
assert mydoc._get_changed_fields() == ["subs.a.b.name"]
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_nested_nested_fields_db_field_set__gets_mark_as_changed_and_cleaned(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField(db_field="db_name")
class MyDoc(Document):
embed = EmbeddedDocumentField(EmbeddedDoc, db_field="db_embed")
name = StringField(db_field="db_name")
MyDoc.drop_collection()
MyDoc(name="testcase1", embed=EmbeddedDoc(name="foo")).save()
mydoc = MyDoc.objects.first()
mydoc.embed.name = "foo1"
assert mydoc.embed._get_changed_fields() == ["db_name"]
assert mydoc._get_changed_fields() == ["db_embed.db_name"]
mydoc = MyDoc.objects.first()
embed = EmbeddedDoc(name="foo2")
embed.name = "bar"
mydoc.embed = embed
assert embed._get_changed_fields() == ["db_name"]
assert mydoc._get_changed_fields() == ["db_embed"]
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_lower_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc().save()
mydoc = MyDoc.objects.first()
mydoc.subs["a"] = EmbeddedDoc()
assert mydoc._get_changed_fields() == ["subs.a"]
subdoc = mydoc.subs["a"]
subdoc.name = "bar"
assert subdoc._get_changed_fields() == ["name"]
assert mydoc._get_changed_fields() == ["subs.a"]
mydoc.save()
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_upper_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc(subs={"a": EmbeddedDoc(name="foo")}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs["a"]
subdoc.name = "bar"
assert subdoc._get_changed_fields() == ["name"]
assert mydoc._get_changed_fields() == ["subs.a.name"]
mydoc.subs["a"] = EmbeddedDoc()
assert mydoc._get_changed_fields() == ["subs.a"]
mydoc.save()
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_referenced_object_changed_attributes(self):
"""Ensures that when you save a new reference to a field, the referenced object isn't altered"""
class Organization(Document):
name = StringField()
class User(Document):
name = StringField()
org = ReferenceField("Organization", required=True)
Organization.drop_collection()
User.drop_collection()
org1 = Organization(name="Org 1")
org1.save()
org2 = Organization(name="Org 2")
org2.save()
user = User(name="Fred", org=org1)
user.save()
org1.reload()
org2.reload()
user.reload()
assert org1.name == "Org 1"
assert org2.name == "Org 2"
assert user.name == "Fred"
user.name = "Harold"
user.org = org2
org2.name = "New Org 2"
assert org2.name == "New Org 2"
user.save()
org2.save()
assert org2.name == "New Org 2"
org2.reload()
assert org2.name == "New Org 2"
def test_delta_for_nested_map_fields(self):
class UInfoDocument(Document):
phone = StringField()
class EmbeddedRole(EmbeddedDocument):
type = StringField()
class EmbeddedUser(EmbeddedDocument):
name = StringField()
roles = MapField(field=EmbeddedDocumentField(EmbeddedRole))
rolist = ListField(field=EmbeddedDocumentField(EmbeddedRole))
info = ReferenceField(UInfoDocument)
class Doc(Document):
users = MapField(field=EmbeddedDocumentField(EmbeddedUser))
num = IntField(default=-1)
Doc.drop_collection()
doc = Doc(num=1)
doc.users["007"] = EmbeddedUser(name="Agent007")
doc.save()
uinfo = UInfoDocument(phone="79089269066")
uinfo.save()
d = Doc.objects(num=1).first()
d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin")
d.users["007"]["rolist"].append(EmbeddedRole(type="oops"))
d.users["007"]["info"] = uinfo
delta = d._delta()
assert True == ("users.007.roles.666" in delta[0])
assert True == ("users.007.rolist" in delta[0])
assert True == ("users.007.info" in delta[0])
assert "superadmin" == delta[0]["users.007.roles.666"]["type"]
assert "oops" == delta[0]["users.007.rolist"][0]["type"]
assert uinfo.id == delta[0]["users.007.info"]
if __name__ == "__main__":
unittest.main()

View File

@ -1,20 +1,20 @@
import unittest
import pytest
from mongoengine import *
from mongoengine.connection import get_db
from tests.utils import MongoDBTestCase
__all__ = ("DynamicTest", )
__all__ = ("TestDynamicDocument",)
class DynamicTest(unittest.TestCase):
class TestDynamicDocument(MongoDBTestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
super().setUp()
class Person(DynamicDocument):
name = StringField()
meta = {'allow_inheritance': True}
meta = {"allow_inheritance": True}
Person.drop_collection()
@ -27,16 +27,28 @@ class DynamicTest(unittest.TestCase):
p.name = "James"
p.age = 34
self.assertEqual(p.to_mongo(), {"_cls": "Person", "name": "James",
"age": 34})
self.assertEqual(p.to_mongo().keys(), ["_cls", "name", "age"])
assert p.to_mongo() == {"_cls": "Person", "name": "James", "age": 34}
assert p.to_mongo().keys() == ["_cls", "name", "age"]
p.save()
self.assertEqual(p.to_mongo().keys(), ["_id", "_cls", "name", "age"])
assert p.to_mongo().keys() == ["_id", "_cls", "name", "age"]
self.assertEqual(self.Person.objects.first().age, 34)
assert self.Person.objects.first().age == 34
# Confirm no changes to self.Person
self.assertFalse(hasattr(self.Person, 'age'))
assert not hasattr(self.Person, "age")
def test_dynamic_document_parse_values_in_constructor_like_document_do(self):
class ProductDynamicDocument(DynamicDocument):
title = StringField()
price = FloatField()
class ProductDocument(Document):
title = StringField()
price = FloatField()
product = ProductDocument(title="Blabla", price="12.5")
dyn_product = ProductDynamicDocument(title="Blabla", price="12.5")
assert product.price == dyn_product.price == 12.5
def test_change_scope_of_variable(self):
"""Test changing the scope of a dynamic field has no adverse effects"""
@ -46,11 +58,11 @@ class DynamicTest(unittest.TestCase):
p.save()
p = self.Person.objects.get()
p.misc = {'hello': 'world'}
p.misc = {"hello": "world"}
p.save()
p = self.Person.objects.get()
self.assertEqual(p.misc, {'hello': 'world'})
assert p.misc == {"hello": "world"}
def test_delete_dynamic_field(self):
"""Test deleting a dynamic field works"""
@ -61,23 +73,23 @@ class DynamicTest(unittest.TestCase):
p.save()
p = self.Person.objects.get()
p.misc = {'hello': 'world'}
p.misc = {"hello": "world"}
p.save()
p = self.Person.objects.get()
self.assertEqual(p.misc, {'hello': 'world'})
assert p.misc == {"hello": "world"}
collection = self.db[self.Person._get_collection_name()]
obj = collection.find_one()
self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'misc', 'name'])
assert sorted(obj.keys()) == ["_cls", "_id", "misc", "name"]
del p.misc
p.save()
p = self.Person.objects.get()
self.assertFalse(hasattr(p, 'misc'))
assert not hasattr(p, "misc")
obj = collection.find_one()
self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'name'])
assert sorted(obj.keys()) == ["_cls", "_id", "name"]
def test_reload_after_unsetting(self):
p = self.Person()
@ -91,12 +103,55 @@ class DynamicTest(unittest.TestCase):
p = self.Person.objects.create()
p.update(age=1)
self.assertEqual(len(p._data), 3)
self.assertEqual(sorted(p._data.keys()), ['_cls', 'id', 'name'])
assert len(p._data) == 3
assert sorted(p._data.keys()) == ["_cls", "id", "name"]
p.reload()
self.assertEqual(len(p._data), 4)
self.assertEqual(sorted(p._data.keys()), ['_cls', 'age', 'id', 'name'])
assert len(p._data) == 4
assert sorted(p._data.keys()) == ["_cls", "age", "id", "name"]
def test_fields_without_underscore(self):
"""Ensure we can query dynamic fields"""
Person = self.Person
p = self.Person(name="Dean")
p.save()
raw_p = Person.objects.as_pymongo().get(id=p.id)
assert raw_p == {"_cls": "Person", "_id": p.id, "name": "Dean"}
p.name = "OldDean"
p.newattr = "garbage"
p.save()
raw_p = Person.objects.as_pymongo().get(id=p.id)
assert raw_p == {
"_cls": "Person",
"_id": p.id,
"name": "OldDean",
"newattr": "garbage",
}
def test_fields_containing_underscore(self):
"""Ensure we can query dynamic fields"""
class WeirdPerson(DynamicDocument):
name = StringField()
_name = StringField()
WeirdPerson.drop_collection()
p = WeirdPerson(name="Dean", _name="Dean")
p.save()
raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id)
assert raw_p == {"_id": p.id, "_name": "Dean", "name": "Dean"}
p.name = "OldDean"
p._name = "NewDean"
p._newattr1 = "garbage" # Unknown fields won't be added
p.save()
raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id)
assert raw_p == {"_id": p.id, "_name": "NewDean", "name": "OldDean"}
def test_dynamic_document_queries(self):
"""Ensure we can query dynamic fields"""
@ -105,10 +160,10 @@ class DynamicTest(unittest.TestCase):
p.age = 22
p.save()
self.assertEqual(1, self.Person.objects(age=22).count())
assert 1 == self.Person.objects(age=22).count()
p = self.Person.objects(age=22)
p = p.get()
self.assertEqual(22, p.age)
assert 22 == p.age
def test_complex_dynamic_document_queries(self):
class Person(DynamicDocument):
@ -128,26 +183,25 @@ class DynamicTest(unittest.TestCase):
p2.age = 10
p2.save()
self.assertEqual(Person.objects(age__icontains='ten').count(), 2)
self.assertEqual(Person.objects(age__gte=10).count(), 1)
assert Person.objects(age__icontains="ten").count() == 2
assert Person.objects(age__gte=10).count() == 1
def test_complex_data_lookups(self):
"""Ensure you can query dynamic document dynamic fields"""
p = self.Person()
p.misc = {'hello': 'world'}
p.misc = {"hello": "world"}
p.save()
self.assertEqual(1, self.Person.objects(misc__hello='world').count())
assert 1 == self.Person.objects(misc__hello="world").count()
def test_three_level_complex_data_lookups(self):
"""Ensure you can query three level document dynamic fields"""
p = self.Person.objects.create(
misc={'hello': {'hello2': 'world'}}
)
self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count())
self.Person.objects.create(misc={"hello": {"hello2": "world"}})
assert 1 == self.Person.objects(misc__hello__hello2="world").count()
def test_complex_embedded_document_validation(self):
"""Ensure embedded dynamic documents may be validated"""
class Embedded(DynamicEmbeddedDocument):
content = URLField()
@ -157,27 +211,29 @@ class DynamicTest(unittest.TestCase):
Doc.drop_collection()
doc = Doc()
embedded_doc_1 = Embedded(content='http://mongoengine.org')
embedded_doc_1 = Embedded(content="http://mongoengine.org")
embedded_doc_1.validate()
embedded_doc_2 = Embedded(content='this is not a url')
self.assertRaises(ValidationError, embedded_doc_2.validate)
embedded_doc_2 = Embedded(content="this is not a url")
with pytest.raises(ValidationError):
embedded_doc_2.validate()
doc.embedded_field_1 = embedded_doc_1
doc.embedded_field_2 = embedded_doc_2
self.assertRaises(ValidationError, doc.validate)
with pytest.raises(ValidationError):
doc.validate()
def test_inheritance(self):
"""Ensure that dynamic document plays nice with inheritance"""
class Employee(self.Person):
salary = IntField()
Employee.drop_collection()
self.assertTrue('name' in Employee._fields)
self.assertTrue('salary' in Employee._fields)
self.assertEqual(Employee._get_collection_name(),
self.Person._get_collection_name())
assert "name" in Employee._fields
assert "salary" in Employee._fields
assert Employee._get_collection_name() == self.Person._get_collection_name()
joe_bloggs = Employee()
joe_bloggs.name = "Joe Bloggs"
@ -185,14 +241,15 @@ class DynamicTest(unittest.TestCase):
joe_bloggs.age = 20
joe_bloggs.save()
self.assertEqual(1, self.Person.objects(age=20).count())
self.assertEqual(1, Employee.objects(age=20).count())
assert 1 == self.Person.objects(age=20).count()
assert 1 == Employee.objects(age=20).count()
joe_bloggs = self.Person.objects.first()
self.assertTrue(isinstance(joe_bloggs, Employee))
assert isinstance(joe_bloggs, Employee)
def test_embedded_dynamic_document(self):
"""Test dynamic embedded documents"""
class Embedded(DynamicEmbeddedDocument):
pass
@ -203,33 +260,33 @@ class DynamicTest(unittest.TestCase):
doc = Doc()
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.string_field = "hello"
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
embedded_1.dict_field = {"hello": "world"}
embedded_1.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field = embedded_1
self.assertEqual(doc.to_mongo(), {
assert doc.to_mongo() == {
"embedded_field": {
"_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ['1', 2, {'hello': 'world'}]
"list_field": ["1", 2, {"hello": "world"}],
}
})
}
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc.embedded_field.__class__, Embedded)
self.assertEqual(doc.embedded_field.string_field, "hello")
self.assertEqual(doc.embedded_field.int_field, 1)
self.assertEqual(doc.embedded_field.dict_field, {'hello': 'world'})
self.assertEqual(doc.embedded_field.list_field,
['1', 2, {'hello': 'world'}])
assert doc.embedded_field.__class__ == Embedded
assert doc.embedded_field.string_field == "hello"
assert doc.embedded_field.int_field == 1
assert doc.embedded_field.dict_field == {"hello": "world"}
assert doc.embedded_field.list_field == ["1", 2, {"hello": "world"}]
def test_complex_embedded_documents(self):
"""Test complex dynamic embedded documents setups"""
class Embedded(DynamicEmbeddedDocument):
pass
@ -240,51 +297,54 @@ class DynamicTest(unittest.TestCase):
doc = Doc()
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.string_field = "hello"
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.dict_field = {"hello": "world"}
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.string_field = "hello"
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
embedded_2.dict_field = {"hello": "world"}
embedded_2.list_field = ["1", 2, {"hello": "world"}]
embedded_1.list_field = ['1', 2, embedded_2]
embedded_1.list_field = ["1", 2, embedded_2]
doc.embedded_field = embedded_1
self.assertEqual(doc.to_mongo(), {
assert doc.to_mongo() == {
"embedded_field": {
"_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ['1', 2,
{"_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ['1', 2, {'hello': 'world'}]}
]
"list_field": [
"1",
2,
{
"_cls": "Embedded",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ["1", 2, {"hello": "world"}],
},
],
}
})
}
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc.embedded_field.__class__, Embedded)
self.assertEqual(doc.embedded_field.string_field, "hello")
self.assertEqual(doc.embedded_field.int_field, 1)
self.assertEqual(doc.embedded_field.dict_field, {'hello': 'world'})
self.assertEqual(doc.embedded_field.list_field[0], '1')
self.assertEqual(doc.embedded_field.list_field[1], 2)
assert doc.embedded_field.__class__ == Embedded
assert doc.embedded_field.string_field == "hello"
assert doc.embedded_field.int_field == 1
assert doc.embedded_field.dict_field == {"hello": "world"}
assert doc.embedded_field.list_field[0] == "1"
assert doc.embedded_field.list_field[1] == 2
embedded_field = doc.embedded_field.list_field[2]
self.assertEqual(embedded_field.__class__, Embedded)
self.assertEqual(embedded_field.string_field, "hello")
self.assertEqual(embedded_field.int_field, 1)
self.assertEqual(embedded_field.dict_field, {'hello': 'world'})
self.assertEqual(embedded_field.list_field, ['1', 2,
{'hello': 'world'}])
assert embedded_field.__class__ == Embedded
assert embedded_field.string_field == "hello"
assert embedded_field.int_field == 1
assert embedded_field.dict_field == {"hello": "world"}
assert embedded_field.list_field == ["1", 2, {"hello": "world"}]
def test_dynamic_and_embedded(self):
"""Ensure embedded documents play nicely"""
@ -303,18 +363,18 @@ class DynamicTest(unittest.TestCase):
person.address.city = "Lundenne"
person.save()
self.assertEqual(Person.objects.first().address.city, "Lundenne")
assert Person.objects.first().address.city == "Lundenne"
person = Person.objects.first()
person.address = Address(city="Londinium")
person.save()
self.assertEqual(Person.objects.first().address.city, "Londinium")
assert Person.objects.first().address.city == "Londinium"
person = Person.objects.first()
person.age = 35
person.save()
self.assertEqual(Person.objects.first().age, 35)
assert Person.objects.first().age == 35
def test_dynamic_embedded_works_with_only(self):
"""Ensure custom fieldnames on a dynamic embedded document are found by qs.only()"""
@ -327,10 +387,15 @@ class DynamicTest(unittest.TestCase):
Person.drop_collection()
Person(name="Eric", address=Address(city="San Francisco", street_number="1337")).save()
Person(
name="Eric", address=Address(city="San Francisco", street_number="1337")
).save()
self.assertEqual(Person.objects.first().address.street_number, '1337')
self.assertEqual(Person.objects.only('address__street_number').first().address.street_number, '1337')
assert Person.objects.first().address.street_number == "1337"
assert (
Person.objects.only("address__street_number").first().address.street_number
== "1337"
)
def test_dynamic_and_embedded_dict_access(self):
"""Ensure embedded dynamic documents work with dict[] style access"""
@ -354,20 +419,21 @@ class DynamicTest(unittest.TestCase):
person["address"]["city"] = "Lundenne"
person.save()
self.assertEqual(Person.objects.first().address.city, "Lundenne")
assert Person.objects.first().address.city == "Lundenne"
self.assertEqual(Person.objects.first().phone, "555-1212")
assert Person.objects.first().phone == "555-1212"
person = Person.objects.first()
person.address = Address(city="Londinium")
person.save()
self.assertEqual(Person.objects.first().address.city, "Londinium")
assert Person.objects.first().address.city == "Londinium"
person = Person.objects.first()
person["age"] = 35
person.save()
self.assertEqual(Person.objects.first().age, 35)
assert Person.objects.first().age == 35
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,617 @@
import unittest
import warnings
import pytest
from mongoengine import (
BooleanField,
Document,
EmbeddedDocument,
EmbeddedDocumentField,
GenericReferenceField,
IntField,
ReferenceField,
StringField,
)
from mongoengine.pymongo_support import list_collection_names
from tests.fixtures import Base
from tests.utils import MongoDBTestCase
class TestInheritance(MongoDBTestCase):
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_constructor_cls(self):
# Ensures _cls is properly set during construction
# and when object gets reloaded (prevent regression of #1950)
class EmbedData(EmbeddedDocument):
data = StringField()
meta = {"allow_inheritance": True}
class DataDoc(Document):
name = StringField()
embed = EmbeddedDocumentField(EmbedData)
meta = {"allow_inheritance": True}
test_doc = DataDoc(name="test", embed=EmbedData(data="data"))
assert test_doc._cls == "DataDoc"
assert test_doc.embed._cls == "EmbedData"
test_doc.save()
saved_doc = DataDoc.objects.with_id(test_doc.id)
assert test_doc._cls == saved_doc._cls
assert test_doc.embed._cls == saved_doc.embed._cls
test_doc.delete()
def test_superclasses(self):
"""Ensure that the correct list of superclasses is assembled."""
class Animal(Document):
meta = {"allow_inheritance": True}
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._superclasses == ()
assert Fish._superclasses == ("Animal",)
assert Guppy._superclasses == ("Animal", "Animal.Fish")
assert Mammal._superclasses == ("Animal",)
assert Dog._superclasses == ("Animal", "Animal.Mammal")
assert Human._superclasses == ("Animal", "Animal.Mammal")
def test_external_superclasses(self):
"""Ensure that the correct list of super classes is assembled when
importing part of the model.
"""
class Animal(Base):
pass
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._superclasses == ("Base",)
assert Fish._superclasses == ("Base", "Base.Animal")
assert Guppy._superclasses == ("Base", "Base.Animal", "Base.Animal.Fish")
assert Mammal._superclasses == ("Base", "Base.Animal")
assert Dog._superclasses == ("Base", "Base.Animal", "Base.Animal.Mammal")
assert Human._superclasses == ("Base", "Base.Animal", "Base.Animal.Mammal")
def test_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled.
"""
class Animal(Document):
meta = {"allow_inheritance": True}
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._subclasses == (
"Animal",
"Animal.Fish",
"Animal.Fish.Guppy",
"Animal.Mammal",
"Animal.Mammal.Dog",
"Animal.Mammal.Human",
)
assert Fish._subclasses == ("Animal.Fish", "Animal.Fish.Guppy")
assert Guppy._subclasses == ("Animal.Fish.Guppy",)
assert Mammal._subclasses == (
"Animal.Mammal",
"Animal.Mammal.Dog",
"Animal.Mammal.Human",
)
assert Human._subclasses == ("Animal.Mammal.Human",)
def test_external_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled when importing part of the model.
"""
class Animal(Base):
pass
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._subclasses == (
"Base.Animal",
"Base.Animal.Fish",
"Base.Animal.Fish.Guppy",
"Base.Animal.Mammal",
"Base.Animal.Mammal.Dog",
"Base.Animal.Mammal.Human",
)
assert Fish._subclasses == ("Base.Animal.Fish", "Base.Animal.Fish.Guppy")
assert Guppy._subclasses == ("Base.Animal.Fish.Guppy",)
assert Mammal._subclasses == (
"Base.Animal.Mammal",
"Base.Animal.Mammal.Dog",
"Base.Animal.Mammal.Human",
)
assert Human._subclasses == ("Base.Animal.Mammal.Human",)
def test_dynamic_declarations(self):
"""Test that declaring an extra class updates meta data"""
class Animal(Document):
meta = {"allow_inheritance": True}
assert Animal._superclasses == ()
assert Animal._subclasses == ("Animal",)
# Test dynamically adding a class changes the meta data
class Fish(Animal):
pass
assert Animal._superclasses == ()
assert Animal._subclasses == ("Animal", "Animal.Fish")
assert Fish._superclasses == ("Animal",)
assert Fish._subclasses == ("Animal.Fish",)
# Test dynamically adding an inherited class changes the meta data
class Pike(Fish):
pass
assert Animal._superclasses == ()
assert Animal._subclasses == ("Animal", "Animal.Fish", "Animal.Fish.Pike")
assert Fish._superclasses == ("Animal",)
assert Fish._subclasses == ("Animal.Fish", "Animal.Fish.Pike")
assert Pike._superclasses == ("Animal", "Animal.Fish")
assert Pike._subclasses == ("Animal.Fish.Pike",)
def test_inheritance_meta_data(self):
"""Ensure that document may inherit fields from a superclass document."""
class Person(Document):
name = StringField()
age = IntField()
meta = {"allow_inheritance": True}
class Employee(Person):
salary = IntField()
assert ["_cls", "age", "id", "name", "salary"] == sorted(
Employee._fields.keys()
)
assert Employee._get_collection_name() == Person._get_collection_name()
def test_inheritance_to_mongo_keys(self):
"""Ensure that document may inherit fields from a superclass document."""
class Person(Document):
name = StringField()
age = IntField()
meta = {"allow_inheritance": True}
class Employee(Person):
salary = IntField()
assert ["_cls", "age", "id", "name", "salary"] == sorted(
Employee._fields.keys()
)
assert Person(name="Bob", age=35).to_mongo().keys() == ["_cls", "name", "age"]
assert Employee(name="Bob", age=35, salary=0).to_mongo().keys() == [
"_cls",
"name",
"age",
"salary",
]
assert Employee._get_collection_name() == Person._get_collection_name()
def test_indexes_and_multiple_inheritance(self):
"""Ensure that all of the indexes are created for a document with
multiple inheritance.
"""
class A(Document):
a = StringField()
meta = {"allow_inheritance": True, "indexes": ["a"]}
class B(Document):
b = StringField()
meta = {"allow_inheritance": True, "indexes": ["b"]}
class C(A, B):
pass
A.drop_collection()
B.drop_collection()
C.drop_collection()
C.ensure_indexes()
assert sorted(
idx["key"] for idx in C._get_collection().index_information().values()
) == sorted([[("_cls", 1), ("b", 1)], [("_id", 1)], [("_cls", 1), ("a", 1)]])
def test_polymorphic_queries(self):
"""Ensure that the correct subclasses are returned from a query"""
class Animal(Document):
meta = {"allow_inheritance": True}
class Fish(Animal):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
Animal.drop_collection()
Animal().save()
Fish().save()
Mammal().save()
Dog().save()
Human().save()
classes = [obj.__class__ for obj in Animal.objects]
assert classes == [Animal, Fish, Mammal, Dog, Human]
classes = [obj.__class__ for obj in Mammal.objects]
assert classes == [Mammal, Dog, Human]
classes = [obj.__class__ for obj in Human.objects]
assert classes == [Human]
def test_allow_inheritance(self):
"""Ensure that inheritance is disabled by default on simple
classes and that _cls will not be used.
"""
class Animal(Document):
name = StringField()
# can't inherit because Animal didn't explicitly allow inheritance
with pytest.raises(ValueError, match="Document Animal may not be subclassed"):
class Dog(Animal):
pass
# Check that _cls etc aren't present on simple documents
dog = Animal(name="dog").save()
assert dog.to_mongo().keys() == ["_id", "name"]
collection = self.db[Animal._get_collection_name()]
obj = collection.find_one()
assert "_cls" not in obj
def test_cant_turn_off_inheritance_on_subclass(self):
"""Ensure if inheritance is on in a subclass you cant turn it off."""
class Animal(Document):
name = StringField()
meta = {"allow_inheritance": True}
with pytest.raises(ValueError) as exc_info:
class Mammal(Animal):
meta = {"allow_inheritance": False}
assert (
str(exc_info.value)
== 'Only direct subclasses of Document may set "allow_inheritance" to False'
)
def test_allow_inheritance_abstract_document(self):
"""Ensure that abstract documents can set inheritance rules and that
_cls will not be used.
"""
class FinalDocument(Document):
meta = {"abstract": True, "allow_inheritance": False}
class Animal(FinalDocument):
name = StringField()
with pytest.raises(ValueError):
class Mammal(Animal):
pass
# Check that _cls isn't present in simple documents
doc = Animal(name="dog")
assert "_cls" not in doc.to_mongo()
def test_using_abstract_class_in_reference_field(self):
# Ensures no regression of #1920
class AbstractHuman(Document):
meta = {"abstract": True}
class Dad(AbstractHuman):
name = StringField()
class Home(Document):
dad = ReferenceField(AbstractHuman) # Referencing the abstract class
address = StringField()
dad = Dad(name="5").save()
Home(dad=dad, address="street").save()
home = Home.objects.first()
home.address = "garbage"
home.save() # Was failing with ValidationError
def test_abstract_class_referencing_self(self):
# Ensures no regression of #1920
class Human(Document):
meta = {"abstract": True}
creator = ReferenceField("self", dbref=True)
class User(Human):
name = StringField()
user = User(name="John").save()
user2 = User(name="Foo", creator=user).save()
user2 = User.objects.with_id(user2.id)
user2.name = "Bar"
user2.save() # Was failing with ValidationError
def test_abstract_handle_ids_in_metaclass_properly(self):
class City(Document):
continent = StringField()
meta = {"abstract": True, "allow_inheritance": False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name="Berlin", continent="Europe")
assert len(berlin._db_field_map) == len(berlin._fields_ordered)
assert len(berlin._reverse_db_field_map) == len(berlin._fields_ordered)
assert len(berlin._fields_ordered) == 3
assert berlin._fields_ordered[0] == "id"
def test_auto_id_not_set_if_specific_in_parent_class(self):
class City(Document):
continent = StringField()
city_id = IntField(primary_key=True)
meta = {"abstract": True, "allow_inheritance": False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name="Berlin", continent="Europe")
assert len(berlin._db_field_map) == len(berlin._fields_ordered)
assert len(berlin._reverse_db_field_map) == len(berlin._fields_ordered)
assert len(berlin._fields_ordered) == 3
assert berlin._fields_ordered[0] == "city_id"
def test_auto_id_vs_non_pk_id_field(self):
class City(Document):
continent = StringField()
id = IntField()
meta = {"abstract": True, "allow_inheritance": False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name="Berlin", continent="Europe")
assert len(berlin._db_field_map) == len(berlin._fields_ordered)
assert len(berlin._reverse_db_field_map) == len(berlin._fields_ordered)
assert len(berlin._fields_ordered) == 4
assert berlin._fields_ordered[0] == "auto_id_0"
berlin.save()
assert berlin.pk == berlin.auto_id_0
def test_abstract_document_creation_does_not_fail(self):
class City(Document):
continent = StringField()
meta = {"abstract": True, "allow_inheritance": False}
city = City(continent="asia")
assert city.pk is None
# TODO: expected error? Shouldn't we create a new error type?
with pytest.raises(KeyError):
city.pk = 1
def test_allow_inheritance_embedded_document(self):
"""Ensure embedded documents respect inheritance."""
class Comment(EmbeddedDocument):
content = StringField()
with pytest.raises(ValueError):
class SpecialComment(Comment):
pass
doc = Comment(content="test")
assert "_cls" not in doc.to_mongo()
class Comment(EmbeddedDocument):
content = StringField()
meta = {"allow_inheritance": True}
doc = Comment(content="test")
assert "_cls" in doc.to_mongo()
def test_document_inheritance(self):
"""Ensure mutliple inheritance of abstract documents"""
class DateCreatedDocument(Document):
meta = {"allow_inheritance": True, "abstract": True}
class DateUpdatedDocument(Document):
meta = {"allow_inheritance": True, "abstract": True}
class MyDocument(DateCreatedDocument, DateUpdatedDocument):
pass
def test_abstract_documents(self):
"""Ensure that a document superclass can be marked as abstract
thereby not using it as the name for the collection."""
defaults = {
"index_background": True,
"index_opts": {"hello": "world"},
"allow_inheritance": True,
"queryset_class": "QuerySet",
"db_alias": "myDB",
"shard_key": ("hello", "world"),
}
meta_settings = {"abstract": True}
meta_settings.update(defaults)
class Animal(Document):
name = StringField()
meta = meta_settings
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
meta = {"abstract": True}
class Human(Mammal):
pass
for k, v in defaults.items():
for cls in [Animal, Fish, Guppy]:
assert cls._meta[k] == v
assert "collection" not in Animal._meta
assert "collection" not in Mammal._meta
assert Animal._get_collection_name() is None
assert Mammal._get_collection_name() is None
assert Fish._get_collection_name() == "fish"
assert Guppy._get_collection_name() == "fish"
assert Human._get_collection_name() == "human"
# ensure that a subclass of a non-abstract class can't be abstract
with pytest.raises(ValueError):
class EvilHuman(Human):
evil = BooleanField(default=True)
meta = {"abstract": True}
def test_abstract_embedded_documents(self):
# 789: EmbeddedDocument shouldn't inherit abstract
class A(EmbeddedDocument):
meta = {"abstract": True}
class B(A):
pass
assert not B._meta["abstract"]
def test_inherited_collections(self):
"""Ensure that subclassed documents don't override parents'
collections
"""
class Drink(Document):
name = StringField()
meta = {"allow_inheritance": True}
class Drinker(Document):
drink = GenericReferenceField()
try:
warnings.simplefilter("error")
class AcloholicDrink(Drink):
meta = {"collection": "booze"}
except SyntaxWarning:
warnings.simplefilter("ignore")
class AlcoholicDrink(Drink):
meta = {"collection": "booze"}
else:
raise AssertionError("SyntaxWarning should be triggered")
warnings.resetwarnings()
Drink.drop_collection()
AlcoholicDrink.drop_collection()
Drinker.drop_collection()
red_bull = Drink(name="Red Bull")
red_bull.save()
programmer = Drinker(drink=red_bull)
programmer.save()
beer = AlcoholicDrink(name="Beer")
beer.save()
real_person = Drinker(drink=beer)
real_person.save()
assert Drinker.objects[0].drink.name == red_bull.name
assert Drinker.objects[1].drink.name == beer.name
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,14 @@
import unittest
import uuid
from nose.plugins.skip import SkipTest
from datetime import datetime
from bson import ObjectId
import pymongo
from mongoengine import *
__all__ = ("TestJson",)
from tests.utils import MongoDBTestCase
class TestJson(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
class TestJson(MongoDBTestCase):
def test_json_names(self):
"""
Going to test reported issue:
@ -25,22 +17,24 @@ class TestJson(unittest.TestCase):
a to_json with the original class names and not the abreviated
mongodb document keys
"""
class Embedded(EmbeddedDocument):
string = StringField(db_field='s')
string = StringField(db_field="s")
class Doc(Document):
string = StringField(db_field='s')
embedded = EmbeddedDocumentField(Embedded, db_field='e')
string = StringField(db_field="s")
embedded = EmbeddedDocumentField(Embedded, db_field="e")
doc = Doc( string="Hello", embedded=Embedded(string="Inner Hello"))
doc_json = doc.to_json(sort_keys=True, use_db_field=False,separators=(',', ':'))
doc = Doc(string="Hello", embedded=Embedded(string="Inner Hello"))
doc_json = doc.to_json(
sort_keys=True, use_db_field=False, separators=(",", ":")
)
expected_json = """{"embedded":{"string":"Inner Hello"},"string":"Hello"}"""
self.assertEqual( doc_json, expected_json)
assert doc_json == expected_json
def test_json_simple(self):
class Embedded(EmbeddedDocument):
string = StringField()
@ -49,22 +43,20 @@ class TestJson(unittest.TestCase):
embedded_field = EmbeddedDocumentField(Embedded)
def __eq__(self, other):
return (self.string == other.string and
self.embedded_field == other.embedded_field)
return (
self.string == other.string
and self.embedded_field == other.embedded_field
)
doc = Doc(string="Hi", embedded_field=Embedded(string="Hi"))
doc_json = doc.to_json(sort_keys=True, separators=(',', ':'))
doc_json = doc.to_json(sort_keys=True, separators=(",", ":"))
expected_json = """{"embedded_field":{"string":"Hi"},"string":"Hi"}"""
self.assertEqual(doc_json, expected_json)
assert doc_json == expected_json
self.assertEqual(doc, Doc.from_json(doc.to_json()))
assert doc == Doc.from_json(doc.to_json())
def test_json_complex(self):
if pymongo.version_tuple[0] <= 2 and pymongo.version_tuple[1] <= 3:
raise SkipTest("Need pymongo 2.4 as has a fix for DBRefs")
class EmbeddedDoc(EmbeddedDocument):
pass
@ -72,41 +64,43 @@ class TestJson(unittest.TestCase):
pass
class Doc(Document):
string_field = StringField(default='1')
string_field = StringField(default="1")
int_field = IntField(default=1)
float_field = FloatField(default=1.1)
boolean_field = BooleanField(default=True)
datetime_field = DateTimeField(default=datetime.now)
embedded_document_field = EmbeddedDocumentField(EmbeddedDoc,
default=lambda: EmbeddedDoc())
embedded_document_field = EmbeddedDocumentField(
EmbeddedDoc, default=lambda: EmbeddedDoc()
)
list_field = ListField(default=lambda: [1, 2, 3])
dict_field = DictField(default=lambda: {"hello": "world"})
objectid_field = ObjectIdField(default=ObjectId)
reference_field = ReferenceField(Simple, default=lambda:
Simple().save())
reference_field = ReferenceField(Simple, default=lambda: Simple().save())
map_field = MapField(IntField(), default=lambda: {"simple": 1})
decimal_field = DecimalField(default=1.0)
complex_datetime_field = ComplexDateTimeField(default=datetime.now)
url_field = URLField(default="http://mongoengine.org")
dynamic_field = DynamicField(default=1)
generic_reference_field = GenericReferenceField(
default=lambda: Simple().save())
sorted_list_field = SortedListField(IntField(),
default=lambda: [1, 2, 3])
default=lambda: Simple().save()
)
sorted_list_field = SortedListField(IntField(), default=lambda: [1, 2, 3])
email_field = EmailField(default="ross@example.com")
geo_point_field = GeoPointField(default=lambda: [1, 2])
sequence_field = SequenceField()
uuid_field = UUIDField(default=uuid.uuid4)
generic_embedded_document_field = GenericEmbeddedDocumentField(
default=lambda: EmbeddedDoc())
default=lambda: EmbeddedDoc()
)
def __eq__(self, other):
import json
return json.loads(self.to_json()) == json.loads(other.to_json())
doc = Doc()
self.assertEqual(doc, Doc.from_json(doc.to_json()))
assert doc == Doc.from_json(doc.to_json())
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,217 @@
import unittest
from datetime import datetime
import pytest
from mongoengine import *
from tests.utils import MongoDBTestCase
class TestValidatorError(MongoDBTestCase):
def test_to_dict(self):
"""Ensure a ValidationError handles error to_dict correctly."""
error = ValidationError("root")
assert error.to_dict() == {}
# 1st level error schema
error.errors = {"1st": ValidationError("bad 1st")}
assert "1st" in error.to_dict()
assert error.to_dict()["1st"] == "bad 1st"
# 2nd level error schema
error.errors = {
"1st": ValidationError(
"bad 1st", errors={"2nd": ValidationError("bad 2nd")}
)
}
assert "1st" in error.to_dict()
assert isinstance(error.to_dict()["1st"], dict)
assert "2nd" in error.to_dict()["1st"]
assert error.to_dict()["1st"]["2nd"] == "bad 2nd"
# moar levels
error.errors = {
"1st": ValidationError(
"bad 1st",
errors={
"2nd": ValidationError(
"bad 2nd",
errors={
"3rd": ValidationError(
"bad 3rd", errors={"4th": ValidationError("Inception")}
)
},
)
},
)
}
assert "1st" in error.to_dict()
assert "2nd" in error.to_dict()["1st"]
assert "3rd" in error.to_dict()["1st"]["2nd"]
assert "4th" in error.to_dict()["1st"]["2nd"]["3rd"]
assert error.to_dict()["1st"]["2nd"]["3rd"]["4th"] == "Inception"
assert error.message == "root(2nd.3rd.4th.Inception: ['1st'])"
def test_model_validation(self):
class User(Document):
username = StringField(primary_key=True)
name = StringField(required=True)
try:
User().validate()
except ValidationError as e:
assert "User:None" in e.message
assert e.to_dict() == {
"username": "Field is required",
"name": "Field is required",
}
user = User(username="RossC0", name="Ross").save()
user.name = None
try:
user.save()
except ValidationError as e:
assert "User:RossC0" in e.message
assert e.to_dict() == {"name": "Field is required"}
def test_fields_rewrite(self):
class BasePerson(Document):
name = StringField()
age = IntField()
meta = {"abstract": True}
class Person(BasePerson):
name = StringField(required=True)
p = Person(age=15)
with pytest.raises(ValidationError):
p.validate()
def test_embedded_document_validation(self):
"""Ensure that embedded documents may be validated."""
class Comment(EmbeddedDocument):
date = DateTimeField()
content = StringField(required=True)
comment = Comment()
with pytest.raises(ValidationError):
comment.validate()
comment.content = "test"
comment.validate()
comment.date = 4
with pytest.raises(ValidationError):
comment.validate()
comment.date = datetime.now()
comment.validate()
assert comment._instance is None
def test_embedded_db_field_validate(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
id = StringField(primary_key=True)
e = EmbeddedDocumentField(SubDoc, db_field="eb")
try:
Doc(id="bad").validate()
except ValidationError as e:
assert "SubDoc:None" in e.message
assert e.to_dict() == {"e": {"val": "OK could not be converted to int"}}
Doc.drop_collection()
Doc(id="test", e=SubDoc(val=15)).save()
doc = Doc.objects.first()
keys = doc._data.keys()
assert 2 == len(keys)
assert "e" in keys
assert "id" in keys
doc.e.val = "OK"
try:
doc.save()
except ValidationError as e:
assert "Doc:test" in e.message
assert e.to_dict() == {"e": {"val": "OK could not be converted to int"}}
def test_embedded_weakref(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
e = EmbeddedDocumentField(SubDoc, db_field="eb")
Doc.drop_collection()
d1 = Doc()
d2 = Doc()
s = SubDoc()
with pytest.raises(ValidationError):
s.validate()
d1.e = s
d2.e = s
del d1
with pytest.raises(ValidationError):
d2.validate()
def test_parent_reference_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited. Issue #954.
"""
class Parent(Document):
meta = {"allow_inheritance": True}
reference = ReferenceField("self")
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child(reference=parent)
# Saving child should not raise a ValidationError
try:
child.save()
except ValidationError as e:
self.fail("ValidationError raised: %s" % e.message)
def test_parent_reference_set_as_attribute_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited and when set via attribute. Issue #954.
"""
class Parent(Document):
meta = {"allow_inheritance": True}
reference = ReferenceField("self")
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child()
child.reference = parent
# Saving the child should not raise a ValidationError
child.save()
if __name__ == "__main__":
unittest.main()

View File

@ -1,214 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from datetime import datetime
from mongoengine import *
__all__ = ("ValidatorErrorTest",)
class ValidatorErrorTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
def test_to_dict(self):
"""Ensure a ValidationError handles error to_dict correctly.
"""
error = ValidationError('root')
self.assertEqual(error.to_dict(), {})
# 1st level error schema
error.errors = {'1st': ValidationError('bad 1st'), }
self.assertTrue('1st' in error.to_dict())
self.assertEqual(error.to_dict()['1st'], 'bad 1st')
# 2nd level error schema
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd'),
})}
self.assertTrue('1st' in error.to_dict())
self.assertTrue(isinstance(error.to_dict()['1st'], dict))
self.assertTrue('2nd' in error.to_dict()['1st'])
self.assertEqual(error.to_dict()['1st']['2nd'], 'bad 2nd')
# moar levels
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd', errors={
'3rd': ValidationError('bad 3rd', errors={
'4th': ValidationError('Inception'),
}),
}),
})}
self.assertTrue('1st' in error.to_dict())
self.assertTrue('2nd' in error.to_dict()['1st'])
self.assertTrue('3rd' in error.to_dict()['1st']['2nd'])
self.assertTrue('4th' in error.to_dict()['1st']['2nd']['3rd'])
self.assertEqual(error.to_dict()['1st']['2nd']['3rd']['4th'],
'Inception')
self.assertEqual(error.message, "root(2nd.3rd.4th.Inception: ['1st'])")
def test_model_validation(self):
class User(Document):
username = StringField(primary_key=True)
name = StringField(required=True)
try:
User().validate()
except ValidationError as e:
self.assertTrue("User:None" in e.message)
self.assertEqual(e.to_dict(), {
'username': 'Field is required',
'name': 'Field is required'})
user = User(username="RossC0", name="Ross").save()
user.name = None
try:
user.save()
except ValidationError as e:
self.assertTrue("User:RossC0" in e.message)
self.assertEqual(e.to_dict(), {
'name': 'Field is required'})
def test_fields_rewrite(self):
class BasePerson(Document):
name = StringField()
age = IntField()
meta = {'abstract': True}
class Person(BasePerson):
name = StringField(required=True)
p = Person(age=15)
self.assertRaises(ValidationError, p.validate)
def test_embedded_document_validation(self):
"""Ensure that embedded documents may be validated.
"""
class Comment(EmbeddedDocument):
date = DateTimeField()
content = StringField(required=True)
comment = Comment()
self.assertRaises(ValidationError, comment.validate)
comment.content = 'test'
comment.validate()
comment.date = 4
self.assertRaises(ValidationError, comment.validate)
comment.date = datetime.now()
comment.validate()
self.assertEqual(comment._instance, None)
def test_embedded_db_field_validate(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
id = StringField(primary_key=True)
e = EmbeddedDocumentField(SubDoc, db_field='eb')
try:
Doc(id="bad").validate()
except ValidationError as e:
self.assertTrue("SubDoc:None" in e.message)
self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}})
Doc.drop_collection()
Doc(id="test", e=SubDoc(val=15)).save()
doc = Doc.objects.first()
keys = doc._data.keys()
self.assertEqual(2, len(keys))
self.assertTrue('e' in keys)
self.assertTrue('id' in keys)
doc.e.val = "OK"
try:
doc.save()
except ValidationError as e:
self.assertTrue("Doc:test" in e.message)
self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}})
def test_embedded_weakref(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
e = EmbeddedDocumentField(SubDoc, db_field='eb')
Doc.drop_collection()
d1 = Doc()
d2 = Doc()
s = SubDoc()
self.assertRaises(ValidationError, s.validate)
d1.e = s
d2.e = s
del d1
self.assertRaises(ValidationError, d2.validate)
def test_parent_reference_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited. Issue #954.
"""
class Parent(Document):
meta = {'allow_inheritance': True}
reference = ReferenceField('self')
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child(reference=parent)
# Saving child should not raise a ValidationError
try:
child.save()
except ValidationError as e:
self.fail("ValidationError raised: %s" % e.message)
def test_parent_reference_set_as_attribute_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited and when set via attribute. Issue #954.
"""
class Parent(Document):
meta = {'allow_inheritance': True}
reference = ReferenceField('self')
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child()
child.reference = parent
# Saving the child should not raise a ValidationError
try:
child.save()
except ValidationError as e:
self.fail("ValidationError raised: %s" % e.message)
if __name__ == '__main__':
unittest.main()

View File

@ -1,3 +0,0 @@
from fields import *
from file_tests import *
from geo import *

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
import uuid
import pytest
from bson import Binary
from mongoengine import *
from tests.utils import MongoDBTestCase
BIN_VALUE = "\xa9\xf3\x8d(\xd7\x03\x84\xb4k[\x0f\xe3\xa2\x19\x85p[J\xa3\xd2>\xde\xe6\x87\xb1\x7f\xc6\xe6\xd9r\x18\xf5".encode(
"latin-1"
)
class TestBinaryField(MongoDBTestCase):
def test_binary_fields(self):
"""Ensure that binary fields can be stored and retrieved."""
class Attachment(Document):
content_type = StringField()
blob = BinaryField()
BLOB = b"\xe6\x00\xc4\xff\x07"
MIME_TYPE = "application/octet-stream"
Attachment.drop_collection()
attachment = Attachment(content_type=MIME_TYPE, blob=BLOB)
attachment.save()
attachment_1 = Attachment.objects().first()
assert MIME_TYPE == attachment_1.content_type
assert BLOB == bytes(attachment_1.blob)
def test_validation_succeeds(self):
"""Ensure that valid values can be assigned to binary fields."""
class AttachmentRequired(Document):
blob = BinaryField(required=True)
class AttachmentSizeLimit(Document):
blob = BinaryField(max_bytes=4)
attachment_required = AttachmentRequired()
with pytest.raises(ValidationError):
attachment_required.validate()
attachment_required.blob = Binary(b"\xe6\x00\xc4\xff\x07")
attachment_required.validate()
_5_BYTES = b"\xe6\x00\xc4\xff\x07"
_4_BYTES = b"\xe6\x00\xc4\xff"
with pytest.raises(ValidationError):
AttachmentSizeLimit(blob=_5_BYTES).validate()
AttachmentSizeLimit(blob=_4_BYTES).validate()
def test_validation_fails(self):
"""Ensure that invalid values cannot be assigned to binary fields."""
class Attachment(Document):
blob = BinaryField()
for invalid_data in (2, "Im_a_unicode", ["some_str"]):
with pytest.raises(ValidationError):
Attachment(blob=invalid_data).validate()
def test__primary(self):
class Attachment(Document):
id = BinaryField(primary_key=True)
Attachment.drop_collection()
binary_id = uuid.uuid4().bytes
att = Attachment(id=binary_id).save()
assert 1 == Attachment.objects.count()
assert 1 == Attachment.objects.filter(id=att.id).count()
att.delete()
assert 0 == Attachment.objects.count()
def test_primary_filter_by_binary_pk_as_str(self):
class Attachment(Document):
id = BinaryField(primary_key=True)
Attachment.drop_collection()
binary_id = uuid.uuid4().bytes
att = Attachment(id=binary_id).save()
assert 1 == Attachment.objects.filter(id=binary_id).count()
att.delete()
assert 0 == Attachment.objects.count()
def test_match_querying_with_bytes(self):
class MyDocument(Document):
bin_field = BinaryField()
MyDocument.drop_collection()
doc = MyDocument(bin_field=BIN_VALUE).save()
matched_doc = MyDocument.objects(bin_field=BIN_VALUE).first()
assert matched_doc.id == doc.id
def test_match_querying_with_binary(self):
class MyDocument(Document):
bin_field = BinaryField()
MyDocument.drop_collection()
doc = MyDocument(bin_field=BIN_VALUE).save()
matched_doc = MyDocument.objects(bin_field=Binary(BIN_VALUE)).first()
assert matched_doc.id == doc.id
def test_modify_operation__set(self):
"""Ensures no regression of bug #1127"""
class MyDocument(Document):
some_field = StringField()
bin_field = BinaryField()
MyDocument.drop_collection()
doc = MyDocument.objects(some_field="test").modify(
upsert=True, new=True, set__bin_field=BIN_VALUE
)
assert doc.some_field == "test"
assert doc.bin_field == BIN_VALUE
def test_update_one(self):
"""Ensures no regression of bug #1127"""
class MyDocument(Document):
bin_field = BinaryField()
MyDocument.drop_collection()
bin_data = b"\xe6\x00\xc4\xff\x07"
doc = MyDocument(bin_field=bin_data).save()
n_updated = MyDocument.objects(bin_field=bin_data).update_one(
bin_field=BIN_VALUE
)
assert n_updated == 1
fetched = MyDocument.objects.with_id(doc.id)
assert fetched.bin_field == BIN_VALUE

View File

@ -0,0 +1,62 @@
import pytest
from mongoengine import *
from tests.utils import MongoDBTestCase, get_as_pymongo
class TestBooleanField(MongoDBTestCase):
def test_storage(self):
class Person(Document):
admin = BooleanField()
person = Person(admin=True)
person.save()
assert get_as_pymongo(person) == {"_id": person.id, "admin": True}
def test_construction_does_not_fail_uncastable_value(self):
class BoolFail:
def __bool__(self):
return "bogus"
class Person(Document):
admin = BooleanField()
person = Person(admin=BoolFail())
person.admin == "bogus"
def test_validation(self):
"""Ensure that invalid values cannot be assigned to boolean
fields.
"""
class Person(Document):
admin = BooleanField()
person = Person()
person.admin = True
person.validate()
person.admin = 2
with pytest.raises(ValidationError):
person.validate()
person.admin = "Yes"
with pytest.raises(ValidationError):
person.validate()
person.admin = "False"
with pytest.raises(ValidationError):
person.validate()
def test_weirdness_constructor(self):
"""When attribute is set in contructor, it gets cast into a bool
which causes some weird behavior. We dont necessarily want to maintain this behavior
but its a known issue
"""
class Person(Document):
admin = BooleanField()
new_person = Person(admin="False")
assert new_person.admin
new_person = Person(admin="0")
assert new_person.admin

View File

@ -0,0 +1,393 @@
from decimal import Decimal
import pytest
from mongoengine import (
CachedReferenceField,
DecimalField,
Document,
EmbeddedDocument,
EmbeddedDocumentField,
InvalidDocumentError,
ListField,
ReferenceField,
StringField,
ValidationError,
)
from tests.utils import MongoDBTestCase
class TestCachedReferenceField(MongoDBTestCase):
def test_constructor_fail_bad_document_type(self):
with pytest.raises(
ValidationError, match="must be a document class or a string"
):
CachedReferenceField(document_type=0)
def test_get_and_save(self):
"""
Tests #1047: CachedReferenceField creates DBRefs on to_python,
but can't save them on to_mongo.
"""
class Animal(Document):
name = StringField()
tag = StringField()
class Ocorrence(Document):
person = StringField()
animal = CachedReferenceField(Animal)
Animal.drop_collection()
Ocorrence.drop_collection()
Ocorrence(
person="testte", animal=Animal(name="Leopard", tag="heavy").save()
).save()
p = Ocorrence.objects.get()
p.person = "new_testte"
p.save()
def test_general_things(self):
class Animal(Document):
name = StringField()
tag = StringField()
class Ocorrence(Document):
person = StringField()
animal = CachedReferenceField(Animal, fields=["tag"])
Animal.drop_collection()
Ocorrence.drop_collection()
a = Animal(name="Leopard", tag="heavy")
a.save()
assert Animal._cached_reference_fields == [Ocorrence.animal]
o = Ocorrence(person="teste", animal=a)
o.save()
p = Ocorrence(person="Wilson")
p.save()
assert Ocorrence.objects(animal=None).count() == 1
assert a.to_mongo(fields=["tag"]) == {"tag": "heavy", "_id": a.pk}
assert o.to_mongo()["animal"]["tag"] == "heavy"
# counts
Ocorrence(person="teste 2").save()
Ocorrence(person="teste 3").save()
count = Ocorrence.objects(animal__tag="heavy").count()
assert count == 1
ocorrence = Ocorrence.objects(animal__tag="heavy").first()
assert ocorrence.person == "teste"
assert isinstance(ocorrence.animal, Animal)
def test_with_decimal(self):
class PersonAuto(Document):
name = StringField()
salary = DecimalField()
class SocialTest(Document):
group = StringField()
person = CachedReferenceField(PersonAuto, fields=("salary",))
PersonAuto.drop_collection()
SocialTest.drop_collection()
p = PersonAuto(name="Alberto", salary=Decimal("7000.00"))
p.save()
s = SocialTest(group="dev", person=p)
s.save()
assert SocialTest.objects._collection.find_one({"person.salary": 7000.00}) == {
"_id": s.pk,
"group": s.group,
"person": {"_id": p.pk, "salary": 7000.00},
}
def test_cached_reference_field_reference(self):
class Group(Document):
name = StringField()
class Person(Document):
name = StringField()
group = ReferenceField(Group)
class SocialData(Document):
obs = StringField()
tags = ListField(StringField())
person = CachedReferenceField(Person, fields=("group",))
Group.drop_collection()
Person.drop_collection()
SocialData.drop_collection()
g1 = Group(name="dev")
g1.save()
g2 = Group(name="designers")
g2.save()
p1 = Person(name="Alberto", group=g1)
p1.save()
p2 = Person(name="Andre", group=g1)
p2.save()
p3 = Person(name="Afro design", group=g2)
p3.save()
s1 = SocialData(obs="testing 123", person=p1, tags=["tag1", "tag2"])
s1.save()
s2 = SocialData(obs="testing 321", person=p3, tags=["tag3", "tag4"])
s2.save()
assert SocialData.objects._collection.find_one({"tags": "tag2"}) == {
"_id": s1.pk,
"obs": "testing 123",
"tags": ["tag1", "tag2"],
"person": {"_id": p1.pk, "group": g1.pk},
}
assert SocialData.objects(person__group=g2).count() == 1
assert SocialData.objects(person__group=g2).first() == s2
def test_cached_reference_field_push_with_fields(self):
class Product(Document):
name = StringField()
Product.drop_collection()
class Basket(Document):
products = ListField(CachedReferenceField(Product, fields=["name"]))
Basket.drop_collection()
product1 = Product(name="abc").save()
product2 = Product(name="def").save()
basket = Basket(products=[product1]).save()
assert Basket.objects._collection.find_one() == {
"_id": basket.pk,
"products": [{"_id": product1.pk, "name": product1.name}],
}
# push to list
basket.update(push__products=product2)
basket.reload()
assert Basket.objects._collection.find_one() == {
"_id": basket.pk,
"products": [
{"_id": product1.pk, "name": product1.name},
{"_id": product2.pk, "name": product2.name},
],
}
def test_cached_reference_field_update_all(self):
class Person(Document):
TYPES = (("pf", "PF"), ("pj", "PJ"))
name = StringField()
tp = StringField(choices=TYPES)
father = CachedReferenceField("self", fields=("tp",))
Person.drop_collection()
a1 = Person(name="Wilson Father", tp="pj")
a1.save()
a2 = Person(name="Wilson Junior", tp="pf", father=a1)
a2.save()
a2 = Person.objects.with_id(a2.id)
assert a2.father.tp == a1.tp
assert dict(a2.to_mongo()) == {
"_id": a2.pk,
"name": "Wilson Junior",
"tp": "pf",
"father": {"_id": a1.pk, "tp": "pj"},
}
assert Person.objects(father=a1)._query == {"father._id": a1.pk}
assert Person.objects(father=a1).count() == 1
Person.objects.update(set__tp="pf")
Person.father.sync_all()
a2.reload()
assert dict(a2.to_mongo()) == {
"_id": a2.pk,
"name": "Wilson Junior",
"tp": "pf",
"father": {"_id": a1.pk, "tp": "pf"},
}
def test_cached_reference_fields_on_embedded_documents(self):
with pytest.raises(InvalidDocumentError):
class Test(Document):
name = StringField()
type(
"WrongEmbeddedDocument",
(EmbeddedDocument,),
{"test": CachedReferenceField(Test)},
)
def test_cached_reference_auto_sync(self):
class Person(Document):
TYPES = (("pf", "PF"), ("pj", "PJ"))
name = StringField()
tp = StringField(choices=TYPES)
father = CachedReferenceField("self", fields=("tp",))
Person.drop_collection()
a1 = Person(name="Wilson Father", tp="pj")
a1.save()
a2 = Person(name="Wilson Junior", tp="pf", father=a1)
a2.save()
a1.tp = "pf"
a1.save()
a2.reload()
assert dict(a2.to_mongo()) == {
"_id": a2.pk,
"name": "Wilson Junior",
"tp": "pf",
"father": {"_id": a1.pk, "tp": "pf"},
}
def test_cached_reference_auto_sync_disabled(self):
class Persone(Document):
TYPES = (("pf", "PF"), ("pj", "PJ"))
name = StringField()
tp = StringField(choices=TYPES)
father = CachedReferenceField("self", fields=("tp",), auto_sync=False)
Persone.drop_collection()
a1 = Persone(name="Wilson Father", tp="pj")
a1.save()
a2 = Persone(name="Wilson Junior", tp="pf", father=a1)
a2.save()
a1.tp = "pf"
a1.save()
assert Persone.objects._collection.find_one({"_id": a2.pk}) == {
"_id": a2.pk,
"name": "Wilson Junior",
"tp": "pf",
"father": {"_id": a1.pk, "tp": "pj"},
}
def test_cached_reference_embedded_fields(self):
class Owner(EmbeddedDocument):
TPS = (("n", "Normal"), ("u", "Urgent"))
name = StringField()
tp = StringField(verbose_name="Type", db_field="t", choices=TPS)
class Animal(Document):
name = StringField()
tag = StringField()
owner = EmbeddedDocumentField(Owner)
class Ocorrence(Document):
person = StringField()
animal = CachedReferenceField(Animal, fields=["tag", "owner.tp"])
Animal.drop_collection()
Ocorrence.drop_collection()
a = Animal(
name="Leopard", tag="heavy", owner=Owner(tp="u", name="Wilson Júnior")
)
a.save()
o = Ocorrence(person="teste", animal=a)
o.save()
assert dict(a.to_mongo(fields=["tag", "owner.tp"])) == {
"_id": a.pk,
"tag": "heavy",
"owner": {"t": "u"},
}
assert o.to_mongo()["animal"]["tag"] == "heavy"
assert o.to_mongo()["animal"]["owner"]["t"] == "u"
# Check to_mongo with fields
assert "animal" not in o.to_mongo(fields=["person"])
# counts
Ocorrence(person="teste 2").save()
Ocorrence(person="teste 3").save()
count = Ocorrence.objects(animal__tag="heavy", animal__owner__tp="u").count()
assert count == 1
ocorrence = Ocorrence.objects(
animal__tag="heavy", animal__owner__tp="u"
).first()
assert ocorrence.person == "teste"
assert isinstance(ocorrence.animal, Animal)
def test_cached_reference_embedded_list_fields(self):
class Owner(EmbeddedDocument):
name = StringField()
tags = ListField(StringField())
class Animal(Document):
name = StringField()
tag = StringField()
owner = EmbeddedDocumentField(Owner)
class Ocorrence(Document):
person = StringField()
animal = CachedReferenceField(Animal, fields=["tag", "owner.tags"])
Animal.drop_collection()
Ocorrence.drop_collection()
a = Animal(
name="Leopard",
tag="heavy",
owner=Owner(tags=["cool", "funny"], name="Wilson Júnior"),
)
a.save()
o = Ocorrence(person="teste 2", animal=a)
o.save()
assert dict(a.to_mongo(fields=["tag", "owner.tags"])) == {
"_id": a.pk,
"tag": "heavy",
"owner": {"tags": ["cool", "funny"]},
}
assert o.to_mongo()["animal"]["tag"] == "heavy"
assert o.to_mongo()["animal"]["owner"]["tags"] == ["cool", "funny"]
# counts
Ocorrence(person="teste 2").save()
Ocorrence(person="teste 3").save()
query = Ocorrence.objects(
animal__tag="heavy", animal__owner__tags="cool"
)._query
assert query == {"animal.owner.tags": "cool", "animal.tag": "heavy"}
ocorrence = Ocorrence.objects(
animal__tag="heavy", animal__owner__tags="cool"
).first()
assert ocorrence.person == "teste 2"
assert isinstance(ocorrence.animal, Animal)

View File

@ -0,0 +1,208 @@
import datetime
import itertools
import math
import re
import pytest
from mongoengine import *
from tests.utils import MongoDBTestCase
class ComplexDateTimeFieldTest(MongoDBTestCase):
def test_complexdatetime_storage(self):
"""Tests for complex datetime fields - which can handle
microseconds without rounding.
"""
class LogEntry(Document):
date = ComplexDateTimeField()
date_with_dots = ComplexDateTimeField(separator=".")
LogEntry.drop_collection()
# Post UTC - microseconds are rounded (down) nearest millisecond and
# dropped - with default datetimefields
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999)
log = LogEntry()
log.date = d1
log.save()
log.reload()
assert log.date == d1
# Post UTC - microseconds are rounded (down) nearest millisecond - with
# default datetimefields
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999)
log.date = d1
log.save()
log.reload()
assert log.date == d1
# Pre UTC dates microseconds below 1000 are dropped - with default
# datetimefields
d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999)
log.date = d1
log.save()
log.reload()
assert log.date == d1
# Pre UTC microseconds above 1000 is wonky - with default datetimefields
# log.date has an invalid microsecond value so I can't construct
# a date to compare.
for i in range(1001, 3113, 33):
d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, i)
log.date = d1
log.save()
log.reload()
assert log.date == d1
log1 = LogEntry.objects.get(date=d1)
assert log == log1
# Test string padding
microsecond = map(int, (math.pow(10, x) for x in range(6)))
mm = dd = hh = ii = ss = [1, 10]
for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond):
stored = LogEntry(date=datetime.datetime(*values)).to_mongo()["date"]
assert (
re.match(r"^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$", stored)
is not None
)
# Test separator
stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()[
"date_with_dots"
]
assert (
re.match(r"^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$", stored) is not None
)
def test_complexdatetime_usage(self):
"""Tests for complex datetime fields - which can handle
microseconds without rounding.
"""
class LogEntry(Document):
date = ComplexDateTimeField()
LogEntry.drop_collection()
d1 = datetime.datetime(1950, 1, 1, 0, 0, 1, 999)
log = LogEntry()
log.date = d1
log.save()
log1 = LogEntry.objects.get(date=d1)
assert log == log1
# create extra 59 log entries for a total of 60
for i in range(1951, 2010):
d = datetime.datetime(i, 1, 1, 0, 0, 1, 999)
LogEntry(date=d).save()
assert LogEntry.objects.count() == 60
# Test ordering
logs = LogEntry.objects.order_by("date")
i = 0
while i < 59:
assert logs[i].date <= logs[i + 1].date
i += 1
logs = LogEntry.objects.order_by("-date")
i = 0
while i < 59:
assert logs[i].date >= logs[i + 1].date
i += 1
# Test searching
logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1))
assert logs.count() == 30
logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1))
assert logs.count() == 30
logs = LogEntry.objects.filter(
date__lte=datetime.datetime(2011, 1, 1),
date__gte=datetime.datetime(2000, 1, 1),
)
assert logs.count() == 10
LogEntry.drop_collection()
# Test microsecond-level ordering/filtering
for microsecond in (99, 999, 9999, 10000):
LogEntry(date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond)).save()
logs = list(LogEntry.objects.order_by("date"))
for next_idx, log in enumerate(logs[:-1], start=1):
next_log = logs[next_idx]
assert log.date < next_log.date
logs = list(LogEntry.objects.order_by("-date"))
for next_idx, log in enumerate(logs[:-1], start=1):
next_log = logs[next_idx]
assert log.date > next_log.date
logs = LogEntry.objects.filter(
date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000)
)
assert logs.count() == 4
def test_no_default_value(self):
class Log(Document):
timestamp = ComplexDateTimeField()
Log.drop_collection()
log = Log()
assert log.timestamp is None
log.save()
fetched_log = Log.objects.with_id(log.id)
assert fetched_log.timestamp is None
def test_default_static_value(self):
NOW = datetime.datetime.utcnow()
class Log(Document):
timestamp = ComplexDateTimeField(default=NOW)
Log.drop_collection()
log = Log()
assert log.timestamp == NOW
log.save()
fetched_log = Log.objects.with_id(log.id)
assert fetched_log.timestamp == NOW
def test_default_callable(self):
NOW = datetime.datetime.utcnow()
class Log(Document):
timestamp = ComplexDateTimeField(default=datetime.datetime.utcnow)
Log.drop_collection()
log = Log()
assert log.timestamp >= NOW
log.save()
fetched_log = Log.objects.with_id(log.id)
assert fetched_log.timestamp >= NOW
def test_setting_bad_value_does_not_raise_unless_validate_is_called(self):
# test regression of #2253
class Log(Document):
timestamp = ComplexDateTimeField()
Log.drop_collection()
log = Log(timestamp="garbage")
with pytest.raises(ValidationError):
log.validate()
with pytest.raises(ValidationError):
log.save()

View File

@ -0,0 +1,163 @@
import datetime
import pytest
try:
import dateutil
except ImportError:
dateutil = None
from mongoengine import *
from tests.utils import MongoDBTestCase
class TestDateField(MongoDBTestCase):
def test_date_from_empty_string(self):
"""
Ensure an exception is raised when trying to
cast an empty string to datetime.
"""
class MyDoc(Document):
dt = DateField()
md = MyDoc(dt="")
with pytest.raises(ValidationError):
md.save()
def test_date_from_whitespace_string(self):
"""
Ensure an exception is raised when trying to
cast a whitespace-only string to datetime.
"""
class MyDoc(Document):
dt = DateField()
md = MyDoc(dt=" ")
with pytest.raises(ValidationError):
md.save()
def test_default_values_today(self):
"""Ensure that default field values are used when creating
a document.
"""
class Person(Document):
day = DateField(default=datetime.date.today)
person = Person()
person.validate()
assert person.day == person.day
assert person.day == datetime.date.today()
assert person._data["day"] == person.day
def test_date(self):
"""Tests showing pymongo date fields
See: http://api.mongodb.org/python/current/api/bson/son.html#dt
"""
class LogEntry(Document):
date = DateField()
LogEntry.drop_collection()
# Test can save dates
log = LogEntry()
log.date = datetime.date.today()
log.save()
log.reload()
assert log.date == datetime.date.today()
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999)
d2 = datetime.datetime(1970, 1, 1, 0, 0, 1)
log = LogEntry()
log.date = d1
log.save()
log.reload()
assert log.date == d1.date()
assert log.date == d2.date()
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999)
d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000)
log.date = d1
log.save()
log.reload()
assert log.date == d1.date()
assert log.date == d2.date()
def test_regular_usage(self):
"""Tests for regular datetime fields"""
class LogEntry(Document):
date = DateField()
LogEntry.drop_collection()
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1)
log = LogEntry()
log.date = d1
log.validate()
log.save()
for query in (d1, d1.isoformat(" ")):
log1 = LogEntry.objects.get(date=query)
assert log == log1
if dateutil:
log1 = LogEntry.objects.get(date=d1.isoformat("T"))
assert log == log1
# create additional 19 log entries for a total of 20
for i in range(1971, 1990):
d = datetime.datetime(i, 1, 1, 0, 0, 1)
LogEntry(date=d).save()
assert LogEntry.objects.count() == 20
# Test ordering
logs = LogEntry.objects.order_by("date")
i = 0
while i < 19:
assert logs[i].date <= logs[i + 1].date
i += 1
logs = LogEntry.objects.order_by("-date")
i = 0
while i < 19:
assert logs[i].date >= logs[i + 1].date
i += 1
# Test searching
logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1))
assert logs.count() == 10
def test_validation(self):
"""Ensure that invalid values cannot be assigned to datetime
fields.
"""
class LogEntry(Document):
time = DateField()
log = LogEntry()
log.time = datetime.datetime.now()
log.validate()
log.time = datetime.date.today()
log.validate()
log.time = datetime.datetime.now().isoformat(" ")
log.validate()
if dateutil:
log.time = datetime.datetime.now().isoformat("T")
log.validate()
log.time = -1
with pytest.raises(ValidationError):
log.validate()
log.time = "ABC"
with pytest.raises(ValidationError):
log.validate()

View File

@ -0,0 +1,232 @@
import datetime as dt
import pytest
try:
import dateutil
except ImportError:
dateutil = None
from mongoengine import *
from mongoengine import connection
from tests.utils import MongoDBTestCase
class TestDateTimeField(MongoDBTestCase):
def test_datetime_from_empty_string(self):
"""
Ensure an exception is raised when trying to
cast an empty string to datetime.
"""
class MyDoc(Document):
dt = DateTimeField()
md = MyDoc(dt="")
with pytest.raises(ValidationError):
md.save()
def test_datetime_from_whitespace_string(self):
"""
Ensure an exception is raised when trying to
cast a whitespace-only string to datetime.
"""
class MyDoc(Document):
dt = DateTimeField()
md = MyDoc(dt=" ")
with pytest.raises(ValidationError):
md.save()
def test_default_value_utcnow(self):
"""Ensure that default field values are used when creating
a document.
"""
class Person(Document):
created = DateTimeField(default=dt.datetime.utcnow)
utcnow = dt.datetime.utcnow()
person = Person()
person.validate()
person_created_t0 = person.created
assert person.created - utcnow < dt.timedelta(seconds=1)
assert person_created_t0 == person.created # make sure it does not change
assert person._data["created"] == person.created
def test_handling_microseconds(self):
"""Tests showing pymongo datetime fields handling of microseconds.
Microseconds are rounded to the nearest millisecond and pre UTC
handling is wonky.
See: http://api.mongodb.org/python/current/api/bson/son.html#dt
"""
class LogEntry(Document):
date = DateTimeField()
LogEntry.drop_collection()
# Test can save dates
log = LogEntry()
log.date = dt.date.today()
log.save()
log.reload()
assert log.date.date() == dt.date.today()
# Post UTC - microseconds are rounded (down) nearest millisecond and
# dropped
d1 = dt.datetime(1970, 1, 1, 0, 0, 1, 999)
d2 = dt.datetime(1970, 1, 1, 0, 0, 1)
log = LogEntry()
log.date = d1
log.save()
log.reload()
assert log.date != d1
assert log.date == d2
# Post UTC - microseconds are rounded (down) nearest millisecond
d1 = dt.datetime(1970, 1, 1, 0, 0, 1, 9999)
d2 = dt.datetime(1970, 1, 1, 0, 0, 1, 9000)
log.date = d1
log.save()
log.reload()
assert log.date != d1
assert log.date == d2
def test_regular_usage(self):
"""Tests for regular datetime fields"""
class LogEntry(Document):
date = DateTimeField()
LogEntry.drop_collection()
d1 = dt.datetime(1970, 1, 1, 0, 0, 1)
log = LogEntry()
log.date = d1
log.validate()
log.save()
for query in (d1, d1.isoformat(" ")):
log1 = LogEntry.objects.get(date=query)
assert log == log1
if dateutil:
log1 = LogEntry.objects.get(date=d1.isoformat("T"))
assert log == log1
# create additional 19 log entries for a total of 20
for i in range(1971, 1990):
d = dt.datetime(i, 1, 1, 0, 0, 1)
LogEntry(date=d).save()
assert LogEntry.objects.count() == 20
# Test ordering
logs = LogEntry.objects.order_by("date")
i = 0
while i < 19:
assert logs[i].date <= logs[i + 1].date
i += 1
logs = LogEntry.objects.order_by("-date")
i = 0
while i < 19:
assert logs[i].date >= logs[i + 1].date
i += 1
# Test searching
logs = LogEntry.objects.filter(date__gte=dt.datetime(1980, 1, 1))
assert logs.count() == 10
logs = LogEntry.objects.filter(date__lte=dt.datetime(1980, 1, 1))
assert logs.count() == 10
logs = LogEntry.objects.filter(
date__lte=dt.datetime(1980, 1, 1), date__gte=dt.datetime(1975, 1, 1)
)
assert logs.count() == 5
def test_datetime_validation(self):
"""Ensure that invalid values cannot be assigned to datetime
fields.
"""
class LogEntry(Document):
time = DateTimeField()
log = LogEntry()
log.time = dt.datetime.now()
log.validate()
log.time = dt.date.today()
log.validate()
log.time = dt.datetime.now().isoformat(" ")
log.validate()
log.time = "2019-05-16 21:42:57.897847"
log.validate()
if dateutil:
log.time = dt.datetime.now().isoformat("T")
log.validate()
log.time = -1
with pytest.raises(ValidationError):
log.validate()
log.time = "ABC"
with pytest.raises(ValidationError):
log.validate()
log.time = "2019-05-16 21:GARBAGE:12"
with pytest.raises(ValidationError):
log.validate()
log.time = "2019-05-16 21:42:57.GARBAGE"
with pytest.raises(ValidationError):
log.validate()
log.time = "2019-05-16 21:42:57.123.456"
with pytest.raises(ValidationError):
log.validate()
def test_parse_datetime_as_str(self):
class DTDoc(Document):
date = DateTimeField()
date_str = "2019-03-02 22:26:01"
# make sure that passing a parsable datetime works
dtd = DTDoc()
dtd.date = date_str
assert isinstance(dtd.date, str)
dtd.save()
dtd.reload()
assert isinstance(dtd.date, dt.datetime)
assert str(dtd.date) == date_str
dtd.date = "January 1st, 9999999999"
with pytest.raises(ValidationError):
dtd.validate()
class TestDateTimeTzAware(MongoDBTestCase):
def test_datetime_tz_aware_mark_as_changed(self):
# Reset the connections
connection._connection_settings = {}
connection._connections = {}
connection._dbs = {}
connect(db="mongoenginetest", tz_aware=True)
class LogEntry(Document):
time = DateTimeField()
LogEntry.drop_collection()
LogEntry(time=dt.datetime(2013, 1, 1, 0, 0, 0)).save()
log = LogEntry.objects.first()
log.time = dt.datetime(2013, 1, 1, 0, 0, 0)
assert ["time"] == log._changed_fields

View File

@ -0,0 +1,140 @@
from decimal import Decimal
import pytest
from mongoengine import DecimalField, Document, ValidationError
from tests.utils import MongoDBTestCase
class TestDecimalField(MongoDBTestCase):
def test_storage(self):
class Person(Document):
float_value = DecimalField(precision=4)
string_value = DecimalField(precision=4, force_string=True)
Person.drop_collection()
values_to_store = [
10,
10.1,
10.11,
"10.111",
Decimal("10.1111"),
Decimal("10.11111"),
]
for store_at_creation in [True, False]:
for value in values_to_store:
# to_python is called explicitly if values were sent in the kwargs of __init__
if store_at_creation:
Person(float_value=value, string_value=value).save()
else:
person = Person.objects.create()
person.float_value = value
person.string_value = value
person.save()
# How its stored
expected = [
{"float_value": 10.0, "string_value": "10.0000"},
{"float_value": 10.1, "string_value": "10.1000"},
{"float_value": 10.11, "string_value": "10.1100"},
{"float_value": 10.111, "string_value": "10.1110"},
{"float_value": 10.1111, "string_value": "10.1111"},
{"float_value": 10.1111, "string_value": "10.1111"},
]
expected.extend(expected)
actual = list(Person.objects.exclude("id").as_pymongo())
assert expected == actual
# How it comes out locally
expected = [
Decimal("10.0000"),
Decimal("10.1000"),
Decimal("10.1100"),
Decimal("10.1110"),
Decimal("10.1111"),
Decimal("10.1111"),
]
expected.extend(expected)
for field_name in ["float_value", "string_value"]:
actual = list(Person.objects().scalar(field_name))
assert expected == actual
def test_save_none(self):
class Person(Document):
value = DecimalField()
Person.drop_collection()
person = Person(value=None)
assert person.value is None
person.save()
fetched_person = Person.objects.first()
fetched_person.value is None
def test_validation(self):
"""Ensure that invalid values cannot be assigned to decimal fields."""
class Person(Document):
height = DecimalField(min_value=Decimal("0.1"), max_value=Decimal("3.5"))
Person.drop_collection()
Person(height=Decimal("1.89")).save()
person = Person.objects.first()
assert person.height == Decimal("1.89")
person.height = "2.0"
person.save()
person.height = 0.01
with pytest.raises(ValidationError):
person.validate()
person.height = Decimal("0.01")
with pytest.raises(ValidationError):
person.validate()
person.height = Decimal("4.0")
with pytest.raises(ValidationError):
person.validate()
person.height = "something invalid"
with pytest.raises(ValidationError):
person.validate()
person_2 = Person(height="something invalid")
with pytest.raises(ValidationError):
person_2.validate()
def test_comparison(self):
class Person(Document):
money = DecimalField()
Person.drop_collection()
Person(money=6).save()
Person(money=7).save()
Person(money=8).save()
Person(money=10).save()
assert 2 == Person.objects(money__gt=Decimal("7")).count()
assert 2 == Person.objects(money__gt=7).count()
assert 2 == Person.objects(money__gt="7").count()
assert 3 == Person.objects(money__gte="7").count()
def test_precision_0(self):
"""prevent regression of a bug that was raising an exception when using precision=0"""
class TestDoc(Document):
d = DecimalField(precision=0)
TestDoc.drop_collection()
td = TestDoc(d=Decimal("12.00032678131263"))
assert td.d == Decimal("12")
def test_precision_negative_raise(self):
"""prevent regression of a bug that was raising an exception when using precision=0"""
with pytest.raises(
ValidationError, match="precision must be a positive integer"
):
class TestDoc(Document):
dneg = DecimalField(precision=-1)

View File

@ -0,0 +1,369 @@
import pytest
from bson import InvalidDocument
from mongoengine import *
from mongoengine.base import BaseDict
from mongoengine.mongodb_support import (
MONGODB_36,
get_mongodb_version,
)
from tests.utils import MongoDBTestCase, get_as_pymongo
class TestDictField(MongoDBTestCase):
def test_storage(self):
class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
info = {"testkey": "testvalue"}
post = BlogPost(info=info).save()
assert get_as_pymongo(post) == {"_id": post.id, "info": info}
def test_validate_invalid_type(self):
class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
invalid_infos = ["my post", ["test", "test"], {1: "test"}]
for invalid_info in invalid_infos:
with pytest.raises(ValidationError):
BlogPost(info=invalid_info).validate()
def test_keys_with_dots_or_dollars(self):
class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
post = BlogPost()
post.info = {"$title": "test"}
with pytest.raises(ValidationError):
post.validate()
post.info = {"nested": {"$title": "test"}}
with pytest.raises(ValidationError):
post.validate()
post.info = {"$title.test": "test"}
with pytest.raises(ValidationError):
post.validate()
post.info = {"nested": {"the.title": "test"}}
if get_mongodb_version() < MONGODB_36:
# MongoDB < 3.6 rejects dots
# To avoid checking the mongodb version from the DictField class
# we rely on MongoDB to reject the data during the save
post.validate()
with pytest.raises(InvalidDocument):
post.save()
else:
post.validate()
post.info = {"dollar_and_dot": {"te$st.test": "test"}}
if get_mongodb_version() < MONGODB_36:
post.validate()
with pytest.raises(InvalidDocument):
post.save()
else:
post.validate()
def test_general_things(self):
"""Ensure that dict types work as expected."""
class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
post = BlogPost(info={"title": "test"})
post.save()
post = BlogPost()
post.info = {"title": "dollar_sign", "details": {"te$t": "test"}}
post.save()
post = BlogPost()
post.info = {"details": {"test": "test"}}
post.save()
post = BlogPost()
post.info = {"details": {"test": 3}}
post.save()
assert BlogPost.objects.count() == 4
assert BlogPost.objects.filter(info__title__exact="test").count() == 1
assert BlogPost.objects.filter(info__details__test__exact="test").count() == 1
post = BlogPost.objects.filter(info__title__exact="dollar_sign").first()
assert "te$t" in post["info"]["details"]
# Confirm handles non strings or non existing keys
assert BlogPost.objects.filter(info__details__test__exact=5).count() == 0
assert BlogPost.objects.filter(info__made_up__test__exact="test").count() == 0
post = BlogPost.objects.create(info={"title": "original"})
post.info.update({"title": "updated"})
post.save()
post.reload()
assert "updated" == post.info["title"]
post.info.setdefault("authors", [])
post.save()
post.reload()
assert post.info["authors"] == []
def test_dictfield_dump_document(self):
"""Ensure a DictField can handle another document's dump."""
class Doc(Document):
field = DictField()
class ToEmbed(Document):
id = IntField(primary_key=True, default=1)
recursive = DictField()
class ToEmbedParent(Document):
id = IntField(primary_key=True, default=1)
recursive = DictField()
meta = {"allow_inheritance": True}
class ToEmbedChild(ToEmbedParent):
pass
to_embed_recursive = ToEmbed(id=1).save()
to_embed = ToEmbed(
id=2, recursive=to_embed_recursive.to_mongo().to_dict()
).save()
doc = Doc(field=to_embed.to_mongo().to_dict())
doc.save()
assert isinstance(doc.field, dict)
assert doc.field == {"_id": 2, "recursive": {"_id": 1, "recursive": {}}}
# Same thing with a Document with a _cls field
to_embed_recursive = ToEmbedChild(id=1).save()
to_embed_child = ToEmbedChild(
id=2, recursive=to_embed_recursive.to_mongo().to_dict()
).save()
doc = Doc(field=to_embed_child.to_mongo().to_dict())
doc.save()
assert isinstance(doc.field, dict)
expected = {
"_id": 2,
"_cls": "ToEmbedParent.ToEmbedChild",
"recursive": {
"_id": 1,
"_cls": "ToEmbedParent.ToEmbedChild",
"recursive": {},
},
}
assert doc.field == expected
def test_dictfield_strict(self):
"""Ensure that dict field handles validation if provided a strict field type."""
class Simple(Document):
mapping = DictField(field=IntField())
Simple.drop_collection()
e = Simple()
e.mapping["someint"] = 1
e.save()
# try creating an invalid mapping
with pytest.raises(ValidationError):
e.mapping["somestring"] = "abc"
e.save()
def test_dictfield_complex(self):
"""Ensure that the dict field can handle the complex types."""
class SettingBase(EmbeddedDocument):
meta = {"allow_inheritance": True}
class StringSetting(SettingBase):
value = StringField()
class IntegerSetting(SettingBase):
value = IntField()
class Simple(Document):
mapping = DictField()
Simple.drop_collection()
e = Simple()
e.mapping["somestring"] = StringSetting(value="foo")
e.mapping["someint"] = IntegerSetting(value=42)
e.mapping["nested_dict"] = {
"number": 1,
"string": "Hi!",
"float": 1.001,
"complex": IntegerSetting(value=42),
"list": [IntegerSetting(value=42), StringSetting(value="foo")],
}
e.save()
e2 = Simple.objects.get(id=e.id)
assert isinstance(e2.mapping["somestring"], StringSetting)
assert isinstance(e2.mapping["someint"], IntegerSetting)
# Test querying
assert Simple.objects.filter(mapping__someint__value=42).count() == 1
assert Simple.objects.filter(mapping__nested_dict__number=1).count() == 1
assert (
Simple.objects.filter(mapping__nested_dict__complex__value=42).count() == 1
)
assert (
Simple.objects.filter(mapping__nested_dict__list__0__value=42).count() == 1
)
assert (
Simple.objects.filter(mapping__nested_dict__list__1__value="foo").count()
== 1
)
# Confirm can update
Simple.objects().update(set__mapping={"someint": IntegerSetting(value=10)})
Simple.objects().update(
set__mapping__nested_dict__list__1=StringSetting(value="Boo")
)
assert (
Simple.objects.filter(mapping__nested_dict__list__1__value="foo").count()
== 0
)
assert (
Simple.objects.filter(mapping__nested_dict__list__1__value="Boo").count()
== 1
)
def test_push_dict(self):
class MyModel(Document):
events = ListField(DictField())
doc = MyModel(events=[{"a": 1}]).save()
raw_doc = get_as_pymongo(doc)
expected_raw_doc = {"_id": doc.id, "events": [{"a": 1}]}
assert raw_doc == expected_raw_doc
MyModel.objects(id=doc.id).update(push__events={})
raw_doc = get_as_pymongo(doc)
expected_raw_doc = {"_id": doc.id, "events": [{"a": 1}, {}]}
assert raw_doc == expected_raw_doc
def test_ensure_unique_default_instances(self):
"""Ensure that every field has it's own unique default instance."""
class D(Document):
data = DictField()
data2 = DictField(default=lambda: {})
d1 = D()
d1.data["foo"] = "bar"
d1.data2["foo"] = "bar"
d2 = D()
assert d2.data == {}
assert d2.data2 == {}
def test_dict_field_invalid_dict_value(self):
class DictFieldTest(Document):
dictionary = DictField(required=True)
DictFieldTest.drop_collection()
test = DictFieldTest(dictionary=None)
test.dictionary # Just access to test getter
with pytest.raises(ValidationError):
test.validate()
test = DictFieldTest(dictionary=False)
test.dictionary # Just access to test getter
with pytest.raises(ValidationError):
test.validate()
def test_dict_field_raises_validation_error_if_wrongly_assign_embedded_doc(self):
class DictFieldTest(Document):
dictionary = DictField(required=True)
DictFieldTest.drop_collection()
class Embedded(EmbeddedDocument):
name = StringField()
embed = Embedded(name="garbage")
doc = DictFieldTest(dictionary=embed)
with pytest.raises(ValidationError) as exc_info:
doc.validate()
error_msg = str(exc_info.value)
assert "'dictionary'" in error_msg
assert "Only dictionaries may be used in a DictField" in error_msg
def test_atomic_update_dict_field(self):
"""Ensure that the entire DictField can be atomically updated."""
class Simple(Document):
mapping = DictField(field=ListField(IntField(required=True)))
Simple.drop_collection()
e = Simple()
e.mapping["someints"] = [1, 2]
e.save()
e.update(set__mapping={"ints": [3, 4]})
e.reload()
assert isinstance(e.mapping, BaseDict)
assert {"ints": [3, 4]} == e.mapping
# try creating an invalid mapping
with pytest.raises(ValueError):
e.update(set__mapping={"somestrings": ["foo", "bar"]})
def test_dictfield_with_referencefield_complex_nesting_cases(self):
"""Ensure complex nesting inside DictField handles dereferencing of ReferenceField(dbref=True | False)"""
# Relates to Issue #1453
class Doc(Document):
s = StringField()
class Simple(Document):
mapping0 = DictField(ReferenceField(Doc, dbref=True))
mapping1 = DictField(ReferenceField(Doc, dbref=False))
mapping2 = DictField(ListField(ReferenceField(Doc, dbref=True)))
mapping3 = DictField(ListField(ReferenceField(Doc, dbref=False)))
mapping4 = DictField(DictField(field=ReferenceField(Doc, dbref=True)))
mapping5 = DictField(DictField(field=ReferenceField(Doc, dbref=False)))
mapping6 = DictField(ListField(DictField(ReferenceField(Doc, dbref=True))))
mapping7 = DictField(ListField(DictField(ReferenceField(Doc, dbref=False))))
mapping8 = DictField(
ListField(DictField(ListField(ReferenceField(Doc, dbref=True))))
)
mapping9 = DictField(
ListField(DictField(ListField(ReferenceField(Doc, dbref=False))))
)
Doc.drop_collection()
Simple.drop_collection()
d = Doc(s="aa").save()
e = Simple()
e.mapping0["someint"] = e.mapping1["someint"] = d
e.mapping2["someint"] = e.mapping3["someint"] = [d]
e.mapping4["someint"] = e.mapping5["someint"] = {"d": d}
e.mapping6["someint"] = e.mapping7["someint"] = [{"d": d}]
e.mapping8["someint"] = e.mapping9["someint"] = [{"d": [d]}]
e.save()
s = Simple.objects.first()
assert isinstance(s.mapping0["someint"], Doc)
assert isinstance(s.mapping1["someint"], Doc)
assert isinstance(s.mapping2["someint"][0], Doc)
assert isinstance(s.mapping3["someint"][0], Doc)
assert isinstance(s.mapping4["someint"]["d"], Doc)
assert isinstance(s.mapping5["someint"]["d"], Doc)
assert isinstance(s.mapping6["someint"][0]["d"], Doc)
assert isinstance(s.mapping7["someint"][0]["d"], Doc)
assert isinstance(s.mapping8["someint"][0]["d"][0], Doc)
assert isinstance(s.mapping9["someint"][0]["d"][0], Doc)

View File

@ -0,0 +1,134 @@
import pytest
from mongoengine import Document, EmailField, ValidationError
from tests.utils import MongoDBTestCase
class TestEmailField(MongoDBTestCase):
def test_generic_behavior(self):
class User(Document):
email = EmailField()
user = User(email="ross@example.com")
user.validate()
user = User(email="ross@example.co.uk")
user.validate()
user = User(
email=("Kofq@rhom0e4klgauOhpbpNdogawnyIKvQS0wk2mjqrgGQ5SaJIazqqWkm7.net")
)
user.validate()
user = User(email="new-tld@example.technology")
user.validate()
user = User(email="ross@example.com.")
with pytest.raises(ValidationError):
user.validate()
# unicode domain
user = User(email="user@пример.рф")
user.validate()
# invalid unicode domain
user = User(email="user@пример")
with pytest.raises(ValidationError):
user.validate()
# invalid data type
user = User(email=123)
with pytest.raises(ValidationError):
user.validate()
def test_email_field_unicode_user(self):
class User(Document):
email = EmailField()
# unicode user shouldn't validate by default...
user = User(email="Dörte@Sörensen.example.com")
with pytest.raises(ValidationError):
user.validate()
# ...but it should be fine with allow_utf8_user set to True
class User(Document):
email = EmailField(allow_utf8_user=True)
user = User(email="Dörte@Sörensen.example.com")
user.validate()
def test_email_field_domain_whitelist(self):
class User(Document):
email = EmailField()
# localhost domain shouldn't validate by default...
user = User(email="me@localhost")
with pytest.raises(ValidationError):
user.validate()
# ...but it should be fine if it's whitelisted
class User(Document):
email = EmailField(domain_whitelist=["localhost"])
user = User(email="me@localhost")
user.validate()
def test_email_domain_validation_fails_if_invalid_idn(self):
class User(Document):
email = EmailField()
invalid_idn = ".google.com"
user = User(email="me@%s" % invalid_idn)
with pytest.raises(ValidationError) as exc_info:
user.validate()
assert "domain failed IDN encoding" in str(exc_info.value)
def test_email_field_ip_domain(self):
class User(Document):
email = EmailField()
valid_ipv4 = "email@[127.0.0.1]"
valid_ipv6 = "email@[2001:dB8::1]"
invalid_ip = "email@[324.0.0.1]"
# IP address as a domain shouldn't validate by default...
user = User(email=valid_ipv4)
with pytest.raises(ValidationError):
user.validate()
user = User(email=valid_ipv6)
with pytest.raises(ValidationError):
user.validate()
user = User(email=invalid_ip)
with pytest.raises(ValidationError):
user.validate()
# ...but it should be fine with allow_ip_domain set to True
class User(Document):
email = EmailField(allow_ip_domain=True)
user = User(email=valid_ipv4)
user.validate()
user = User(email=valid_ipv6)
user.validate()
# invalid IP should still fail validation
user = User(email=invalid_ip)
with pytest.raises(ValidationError):
user.validate()
def test_email_field_honors_regex(self):
class User(Document):
email = EmailField(regex=r"\w+@example.com")
# Fails regex validation
user = User(email="me@foo.com")
with pytest.raises(ValidationError):
user.validate()
# Passes regex validation
user = User(email="me@example.com")
assert user.validate() is None

Some files were not shown because too many files have changed in this diff Show More