Compare commits

...

28 commits

Author SHA1 Message Date
Markus Unterwaditzer
7fdff404e6 No wheels 2017-12-04 20:16:29 +01:00
Markus Unterwaditzer
1bdde25c0c Fix etesync build 2017-12-04 19:52:02 +01:00
Markus Unterwaditzer
b32932bd13 Relax recurrence tests 2017-12-03 14:00:21 +01:00
Markus Unterwaditzer
22d009b824 Remove unnecessary filter 2017-11-27 19:52:15 +01:00
Markus Unterwaditzer
792dbc171f Fix missing XML header, see #688 2017-11-25 14:15:14 +01:00
Markus Unterwaditzer
5700c4688b
rustup (#686)
* rustup

* rust-vobject upgrade
2017-11-07 21:58:17 +01:00
Markus Unterwaditzer
3984f547ce
Update nextcloud (#684) 2017-11-05 15:59:42 +01:00
Markus Unterwaditzer
9769dab02e
Update owncloud (#685) 2017-11-05 15:59:34 +01:00
Markus Unterwaditzer
bd2e09a84b Small refactor in dav.py 2017-10-26 02:22:18 +02:00
Markus Unterwaditzer
f7b6e67095 Ignore new flake8 linters 2017-10-26 01:41:43 +02:00
Markus Unterwaditzer
a2c509adf5 rustup, fix broken struct export 2017-10-25 22:36:28 +02:00
Markus Unterwaditzer
28fdf42238 Fix #681 2017-10-21 17:23:41 +02:00
Markus Unterwaditzer
0d3b028b17 Cache rust artifacts 2017-10-19 23:47:20 +02:00
Markus Unterwaditzer
f8e65878d8 Update rust installation instructions 2017-10-19 23:41:43 +02:00
Markus Unterwaditzer
75e83cd0f6 Commit cargo.lock 2017-10-19 23:27:29 +02:00
Malte Kiefer
96a8ab35c3 fixed typo (#678)
fixed typo
2017-10-13 19:34:37 +02:00
Markus Unterwaditzer
619373a8e8 Rust: new item module 2017-10-11 13:53:10 +02:00
Markus Unterwaditzer
cbb15e1895 Move all target to top again 2017-10-11 13:28:00 +02:00
Markus Unterwaditzer
325304c50f Lazy-load component in item 2017-10-11 12:01:52 +02:00
Markus Unterwaditzer
bdbfc360ff Move item hashing into rust 2017-10-10 00:52:58 +02:00
Markus Unterwaditzer
c17fa308fb Adapt virtualenv steps to always select python3 2017-10-06 18:32:17 +02:00
Markus Unterwaditzer
81f7472e3a Update installation instructions for Rust dependencies 2017-10-06 18:30:10 +02:00
Markus Unterwaditzer
69543b8615 Install rust on readthedocs 2017-10-05 17:45:19 +02:00
Markus Unterwaditzer
1b7cb4e656 Use rust-vobject (#675)
Use rust-vobject
2017-10-04 22:41:18 +02:00
Markus Unterwaditzer
7bdb22a207 Fix Ubuntu package name of Python 3. 2017-10-03 22:48:13 +02:00
Markus Unterwaditzer
cb41a9df28 Add fast_finish to Travis 2017-10-03 20:59:43 +02:00
Markus Unterwaditzer
33f96f5eca Fix broken link 2017-10-03 13:13:44 +02:00
Markus Unterwaditzer
178ac237ad Fix installation link 2017-10-03 11:29:51 +02:00
34 changed files with 1043 additions and 279 deletions

1
.gitignore vendored
View file

@ -12,4 +12,5 @@ env
dist
docs/_build/
vdirsyncer/version.py
vdirsyncer/_native*
.hypothesis

View file

@ -5,7 +5,12 @@
"master"
]
},
"cache": "pip",
"cache": {
"directories": [
"./rust/target/"
],
"pip": true
},
"dist": "trusty",
"git": {
"submodules": false
@ -19,6 +24,7 @@
],
"language": "python",
"matrix": {
"fast_finish": true,
"include": [
{
"env": "BUILD=style",

View file

@ -9,6 +9,12 @@ Package maintainers and users who have to manually update their installation
may want to subscribe to `GitHub's tag feed
<https://github.com/pimutils/vdirsyncer/tags.atom>`_.
Version 0.17.0
==============
- Fix bug where collection discovery under DAV-storages would produce invalid
XML. See :gh:`688`.
Version 0.16.3
==============

View file

@ -36,8 +36,7 @@ ifeq ($(COVERAGE), true)
endif
ifeq ($(ETESYNC_TESTS), true)
TEST_EXTRA_PACKAGES += git+https://github.com/etesync/journal-manager
TEST_EXTRA_PACKAGES += django djangorestframework wsgi_intercept drf-nested-routers
TEST_EXTRA_PACKAGES += django-etesync-journal django djangorestframework wsgi_intercept drf-nested-routers
endif
PYTEST = py.test $(PYTEST_ARGS)
@ -45,6 +44,9 @@ PYTEST = py.test $(PYTEST_ARGS)
export TESTSERVER_BASE := ./tests/storage/servers/
CODECOV_PATH = /tmp/codecov.sh
all:
$(error Take a look at https://vdirsyncer.pimutils.org/en/stable/tutorial.html#installation)
ifeq ($(CI), true)
test:
curl -s https://codecov.io/bash > $(CODECOV_PATH)
@ -59,9 +61,6 @@ test:
$(PYTEST)
endif
all:
$(error Take a look at https://vdirsyncer.pimutils.org/en/stable/tutorial.html#installation)
install-servers:
set -ex; \
for server in $(DAV_SERVER); do \
@ -104,7 +103,7 @@ linkcheck:
sphinx-build -W -b linkcheck ./docs/ ./docs/_build/linkcheck/
release:
python setup.py sdist bdist_wheel upload
python setup.py sdist upload
release-deb:
sh scripts/release-deb.sh debian jessie
@ -114,7 +113,7 @@ release-deb:
sh scripts/release-deb.sh ubuntu zesty
install-dev:
pip install -e .
pip install -ve .
[ "$(ETESYNC_TESTS)" = "false" ] || pip install -e .[etesync]
set -xe && if [ "$(REQUIREMENTS)" = "devel" ]; then \
pip install -U --force-reinstall \
@ -139,4 +138,11 @@ ssh-submodule-urls:
echo -n 'New URL: '; \
git remote get-url origin"
install-rust:
curl https://sh.rustup.rs -sSf | sh -s -- -y
rust-ext:
[ "$$READTHEDOCS" != "True" ] || $(MAKE) install-rust
cd ./rust && PATH="$$HOME/.cargo/bin/:$$PATH" cargo build --release
.PHONY: docs

View file

@ -12,7 +12,7 @@ writing:
- `ArchLinux (AUR) <https://aur.archlinux.org/packages/vdirsyncer>`_
- `Ubuntu and Debian, x86_64-only
<https://packagecloud.io/pimutils/vdirsyncer/install>`_ (packages also exist
<https://packagecloud.io/pimutils/vdirsyncer>`_ (packages also exist
in the official repositories but may be out of date)
- `GNU Guix <https://www.gnu.org/software/guix/package-list.html#vdirsyncer>`_
- `OS X (homebrew) <http://braumeister.org/formula/vdirsyncer>`_
@ -44,12 +44,17 @@ following things are installed:
- Python 3.4+ and pip.
- ``libxml`` and ``libxslt``
- ``zlib``
- Linux or OS X. **Windows is not supported, see :gh:`535`.**
- `Rust <https://www.rust-lang.org/>`, the programming language, together with
its package manager ``cargo``.
- Linux or OS X. **Windows is not supported**, see :gh:`535`.
On Linux systems, using the distro's package manager is the best
way to do this, for example, using Ubuntu::
On Linux systems, using the distro's package manager is the best way to do
this, for example, using Ubuntu (last tried on Trusty)::
sudo apt-get install libxml2 libxslt1.1 zlib1g python
sudo apt-get install python3 python3-pip libffi-dev
Rust may need to be installed separately, as the packages in Ubuntu are usually
out-of-date. I recommend `rustup <https://rustup.rs/>`_ for that.
Then you have several options. The following text applies for most Python
software by the way.
@ -59,11 +64,14 @@ The dirty, easy way
The easiest way to install vdirsyncer at this point would be to run::
pip install --user --ignore-installed vdirsyncer
pip3 install -v --user --ignore-installed vdirsyncer
- ``--user`` is to install without root rights (into your home directory)
- ``--ignore-installed`` is to work around Debian's potentially broken packages
(see :ref:`debian-urllib3`).
(see :ref:`debian-urllib3`). You can try to omit it if you run into other
problems related to certificates, for example.
Your executable is then in ``~/.local/bin/``.
This method has a major flaw though: Pip doesn't keep track of the files it
installs. Vdirsyncer's files would be located somewhere in
@ -79,9 +87,9 @@ There is a way to install Python software without scattering stuff across
your filesystem: virtualenv_. There are a lot of resources on how to use it,
the simplest possible way would look something like::
virtualenv ~/vdirsyncer_env
~/vdirsyncer_env/bin/pip install vdirsyncer
alias vdirsyncer="~/vdirsyncer_env/bin/vdirsyncer
virtualenv --python python3 ~/vdirsyncer_env
~/vdirsyncer_env/bin/pip install -v vdirsyncer
alias vdirsyncer="$HOME/vdirsyncer_env/bin/vdirsyncer"
You'll have to put the last line into your ``.bashrc`` or ``.bash_profile``.

View file

@ -86,7 +86,7 @@ Crontab
On the end we create a crontab, so that vdirsyncer syncs automatically
every 30 minutes our contacts::
contab -e
crontab -e
On the end of that file enter this line::

1
rust/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

454
rust/Cargo.lock generated Normal file
View file

@ -0,0 +1,454 @@
[root]
name = "vdirsyncer_rustext"
version = "0.1.0"
dependencies = [
"cbindgen 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)",
"ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
"vobject 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ansi_term"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "atty"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace-sys"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bitflags"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cbindgen"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"clap 2.27.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cc"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "clap"
version = "2.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "coco"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dbghelp-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dtoa"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "either"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "error-chain"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fuchsia-zircon"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "futures"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "gcc"
version = "0.3.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itoa"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lazy_static"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "log"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num-traits"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num_cpus"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rand"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon-core"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ring"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "scopeguard"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_codegen_internals"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_derive"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_codegen_internals 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_json"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "strsim"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "syn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
"synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "synom"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tempdir"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termion"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "textwrap"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "toml"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-width"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "untrusted"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "vec_map"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "vobject"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860"
"checksum backtrace 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "99f2ce94e22b8e664d95c57fff45b98a966c2252b60691d0b7aeeccd88d70983"
"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
"checksum cbindgen 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "a4f329332ea243c7bf4ca3fc8a0c35884ab6de60b4c4a2bb89238ec6947a75cb"
"checksum cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b13a57efd6b30ecd6598ebdb302cca617930b5470647570468a65d12ef9719"
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
"checksum clap 2.27.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b8c532887f1a292d17de05ae858a8fe50a301e196f9ef0ddb7ccd0d1d00f180"
"checksum coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c06169f5beb7e31c7c67ebf5540b8b472d23e3eade3b2ec7d1f5b504a85f91bd"
"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum either 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e311a7479512fbdf858fb54d91ec59f3b9f85bc0113659f46bba12b199d273ce"
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
"checksum fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f6c0581a4e363262e52b87f59ee2afe3415361c6ec35e665924eb08afe8ff159"
"checksum fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43f3795b4bae048dc6123a6b972cadde2e676f9ded08aef6bb77f5f157684a82"
"checksum futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "118b49cac82e04121117cbd3121ede3147e885627d82c4546b87c702debb90c1"
"checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb"
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c9e5e58fa1a4c3b915a561a78a22ee0cac6ab97dca2504428bc1cb074375f8d5"
"checksum libc 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "5ba3df4dcb460b9dfbd070d41c94c19209620c191b0340b929ce748a2bcd42d2"
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
"checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0"
"checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum rand 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "61efcbcd9fa8d8fbb07c84e34a8af18a1ff177b449689ad38a6e9457ecc7b2ae"
"checksum rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b614fe08b6665cb9a231d07ac1364b0ef3cb3698f1239ee0c4c3a88a524f54c8"
"checksum rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7febc28567082c345f10cddc3612c6ea020fc3297a1977d472cf9fdb73e6e493"
"checksum redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "8dde11f18c108289bef24469638a04dce49da56084f2d50618b226e47eb04509"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6f7d28b30a72c01b458428e0ae988d4149c20d902346902be881e3edc4bb325c"
"checksum rustc-demangle 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aee45432acc62f7b9a108cc054142dac51f979e69e71ddce7d6fc7adf29e817e"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af"
"checksum serde_codegen_internals 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bc888bd283bd2420b16ad0d860e35ad8acb21941180a83a189bb2046f9d00400"
"checksum serde_derive 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "978fd866f4d4872084a81ccc35e275158351d3b9fe620074e7d7504b816b74ba"
"checksum serde_json 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ad8bcf487be7d2e15d3d543f04312de991d631cfe1b43ea0ade69e6a8a5b16a1"
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
"checksum toml 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bd86ad9ebee246fdedd610e0f6d0587b754a3d81438db930a244d0480ed7878f"
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
"checksum untrusted 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f392d7819dbe58833e26872f5f6f0d68b7bbbe90fc3667e98731c4a15ad9a7ae"
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
"checksum vobject 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9ce763ee7201cb2915eb28450a2a1361e6fdc1650ff2ed806aa3d3216df92141"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"

16
rust/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "vdirsyncer_rustext"
version = "0.1.0"
authors = ["Markus Unterwaditzer <markus@unterwaditzer.net>"]
build = "build.rs"
[lib]
name = "vdirsyncer_rustext"
crate-type = ["cdylib"]
[dependencies]
vobject = "0.4.0"
ring = "0.12.1"
[build-dependencies]
cbindgen = "0.1"

12
rust/build.rs Normal file
View file

@ -0,0 +1,12 @@
extern crate cbindgen;
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let mut config: cbindgen::Config = Default::default();
config.language = cbindgen::Language::C;
cbindgen::generate_with_config(&crate_dir, config)
.unwrap()
.write_to_file("target/vdirsyncer_rustext.h");
}

145
rust/src/item.rs Normal file
View file

@ -0,0 +1,145 @@
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::mem;
use std::ptr;
use vobject;
use ring;
use std::fmt::Write;
use VdirsyncerError;
const EMPTY_STRING: *const c_char = b"\0" as *const u8 as *const c_char;
pub struct VdirsyncerComponent(vobject::Component);
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_get_uid(c: *mut VdirsyncerComponent) -> *const c_char {
match safe_get_uid(&(*c).0) {
Some(x) => CString::new(x).unwrap().into_raw(),
None => EMPTY_STRING
}
}
#[inline]
fn safe_get_uid(c: &vobject::Component) -> Option<String> {
let mut stack = vec![c];
while let Some(vobj) = stack.pop() {
if let Some(prop) = vobj.get_only("UID") {
return Some(prop.value_as_string());
}
stack.extend(vobj.subcomponents.iter());
};
None
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_parse_component(s: *const c_char, err: *mut VdirsyncerError) -> *mut VdirsyncerComponent {
let cstring = CStr::from_ptr(s);
match vobject::parse_component(cstring.to_str().unwrap()) {
Ok(x) => mem::transmute(Box::new(VdirsyncerComponent(x))),
Err(e) => {
(*err).failed = true;
(*err).msg = CString::new(e.description()).unwrap().into_raw();
mem::zeroed()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_component(c: *mut VdirsyncerComponent) {
let _: Box<VdirsyncerComponent> = mem::transmute(c);
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_clear_err(e: *mut VdirsyncerError) {
CString::from_raw((*e).msg);
(*e).msg = ptr::null_mut();
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_change_uid(c: *mut VdirsyncerComponent, uid: *const c_char) {
let uid_cstring = CStr::from_ptr(uid);
change_uid(&mut (*c).0, uid_cstring.to_str().unwrap());
}
fn change_uid(c: &mut vobject::Component, uid: &str) {
let mut stack = vec![c];
while let Some(component) = stack.pop() {
match component.name.as_ref() {
"VEVENT" | "VTODO" | "VJOURNAL" | "VCARD" => {
if !uid.is_empty() {
component.set(vobject::Property::new("UID", uid));
} else {
component.remove("UID");
}
},
_ => ()
}
stack.extend(component.subcomponents.iter_mut());
}
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_clone_component(c: *mut VdirsyncerComponent) -> *mut VdirsyncerComponent {
mem::transmute(Box::new((*c).0.clone()))
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_write_component(c: *mut VdirsyncerComponent) -> *const c_char {
CString::new(vobject::write_component(&(*c).0)).unwrap().into_raw()
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_hash_component(c: *mut VdirsyncerComponent) -> *const c_char {
CString::new(safe_hash_component(&(*c).0)).unwrap().into_raw()
}
fn safe_hash_component(c: &vobject::Component) -> String {
let mut new_c = c.clone();
{
let mut stack = vec![&mut new_c];
while let Some(component) = stack.pop() {
// PRODID is changed by radicale for some reason after upload
component.remove("PRODID");
// Sometimes METHOD:PUBLISH is added by WebCAL providers, for us it doesn't make a difference
component.remove("METHOD");
// X-RADICALE-NAME is used by radicale, because hrefs don't really exist in their filesystem backend
component.remove("X-RADICALE-NAME");
// Apparently this is set by Horde?
// https://github.com/pimutils/vdirsyncer/issues/318
component.remove("X-WR-CALNAME");
// Those are from the VCARD specification and is supposed to change when the
// item does -- however, we can determine that ourselves
component.remove("REV");
component.remove("LAST-MODIFIED");
component.remove("CREATED");
// Some iCalendar HTTP calendars generate the DTSTAMP at request time, so
// this property always changes when the rest of the item didn't. Some do
// the same with the UID.
//
// - Google's read-only calendar links
// - http://www.feiertage-oesterreich.at/
component.remove("DTSTAMP");
component.remove("UID");
if component.name == "VCALENDAR" {
component.subcomponents.retain(|ref c| c.name != "VTIMEZONE");
}
stack.extend(component.subcomponents.iter_mut());
}
}
// FIXME: Possible optimization: Stream component to hasher instead of allocating new string
let digest = ring::digest::digest(&ring::digest::SHA256, vobject::write_component(&new_c).as_bytes());
let mut rv = String::new();
for &byte in digest.as_ref() {
write!(&mut rv, "{:x}", byte).unwrap();
}
rv
}

19
rust/src/lib.rs Normal file
View file

@ -0,0 +1,19 @@
extern crate vobject;
extern crate ring;
use std::ffi::CStr;
use std::os::raw::c_char;
pub mod item;
#[repr(C)]
pub struct VdirsyncerError {
pub failed: bool,
pub msg: *mut c_char,
}
#[no_mangle]
pub unsafe extern "C" fn vdirsyncer_free_str(s: *const c_char) {
CStr::from_ptr(s);
}

View file

@ -8,9 +8,12 @@ ARG distrover
RUN apt-get update
RUN apt-get install -y build-essential fakeroot debhelper git
RUN apt-get install -y python3-all python3-pip
RUN apt-get install -y python3-all python3-dev python3-pip
RUN apt-get install -y ruby ruby-dev
RUN apt-get install -y python-all python-pip
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN apt-get install -y libssl-dev libffi-dev
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN gem install fpm
@ -24,7 +27,7 @@ RUN mkdir /vdirsyncer/pkgs/
RUN basename *.tar.gz .tar.gz | cut -d'-' -f2 | sed -e 's/\.dev/~/g' | tee version
RUN (echo -n *.tar.gz; echo '[google]') | tee requirements.txt
RUN . /vdirsyncer/env/bin/activate; fpm -s virtualenv -t deb \
RUN . /vdirsyncer/env/bin/activate; fpm --verbose -s virtualenv -t deb \
-n "vdirsyncer-latest" \
-v "$(cat version)" \
--prefix /opt/venvs/vdirsyncer-latest \

View file

@ -10,7 +10,12 @@ cfg = {}
cfg['sudo'] = True
cfg['dist'] = 'trusty'
cfg['language'] = 'python'
cfg['cache'] = 'pip'
cfg['cache'] = {
'pip': True,
'directories': [
'./rust/target/'
]
}
cfg['git'] = {
'submodules': False
@ -31,7 +36,7 @@ make -e install-$BUILD
cfg['script'] = ["make -e $BUILD"]
matrix = []
cfg['matrix'] = {'include': matrix}
cfg['matrix'] = {'include': matrix, 'fast_finish': True}
matrix.append({
'python': latest_python,

View file

@ -8,3 +8,6 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then
virtualenv -p python3 $HOME/osx-py3
. $HOME/osx-py3/bin/activate
fi
make install-rust
export PATH="$HOME/.cargo/bin/:$PATH"

View file

@ -1,13 +1,11 @@
[wheel]
universal = 1
[tool:pytest]
norecursedirs = tests/storage/servers/*
addopts = --tb=short
[flake8]
# E731: Use a def instead of lambda expr
ignore = E731
# E743: Ambiguous function definition
ignore = E731, E743
select = C,E,F,W,B,B9
exclude = tests/storage/servers/owncloud/, tests/storage/servers/nextcloud/, tests/storage/servers/baikal/, build/
exclude = .eggs/, tests/storage/servers/owncloud/, tests/storage/servers/nextcloud/, tests/storage/servers/baikal/, build/, vdirsyncer/_native*
application-package-names = tests,vdirsyncer

View file

@ -9,6 +9,7 @@ how to package vdirsyncer.
from setuptools import Command, find_packages, setup
milksnake = 'milksnake'
requirements = [
# https://github.com/mitsuhiko/click/issues/200
@ -32,10 +33,29 @@ requirements = [
'requests_toolbelt >=0.4.0',
# https://github.com/untitaker/python-atomicwrites/commit/4d12f23227b6a944ab1d99c507a69fdbc7c9ed6d # noqa
'atomicwrites>=0.1.7'
'atomicwrites>=0.1.7',
milksnake
]
def build_native(spec):
build = spec.add_external_build(
cmd=['make', 'rust-ext'],
path='.'
)
spec.add_cffi_module(
module_path='vdirsyncer._native',
dylib=lambda: build.find_dylib(
'vdirsyncer_rustext', in_path='rust/target/release'),
header_filename=lambda: build.find_header(
'vdirsyncer_rustext.h', in_path='rust/target'),
# Rust bug: If thread-local storage is used, this flag is necessary
# (mitsuhiko)
rtld_flags=['NODELETE']
)
class PrintRequirements(Command):
description = 'Prints minimal requirements'
user_options = []
@ -75,7 +95,10 @@ setup(
},
# Build dependencies
setup_requires=['setuptools_scm != 1.12.0'],
setup_requires=[
'setuptools_scm != 1.12.0',
milksnake,
],
# Other
packages=find_packages(exclude=['tests.*', 'tests']),
@ -101,4 +124,7 @@ setup(
'Topic :: Internet',
'Topic :: Utilities',
],
milksnake_tasks=[build_native],
zip_safe=False,
platforms='any'
)

View file

@ -3,9 +3,11 @@
Test suite for vdirsyncer.
'''
import random
import hypothesis.strategies as st
from vdirsyncer.vobject import normalize_item
from vdirsyncer.vobject import Item
import urllib3
import urllib3.exceptions
@ -18,7 +20,7 @@ def blow_up(*a, **kw):
def assert_item_equals(a, b):
assert normalize_item(a) == normalize_item(b)
assert a.hash == b.hash
VCARD_TEMPLATE = u'''BEGIN:VCARD
@ -109,3 +111,10 @@ uid_strategy = st.text(
)),
min_size=1
).filter(lambda x: x.strip() == x)
def format_item(uid=None, item_template=VCARD_TEMPLATE):
# assert that special chars are handled correctly.
r = random.random()
uid = uid or r
return Item(item_template.format(r=r, uid=uid))

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
import random
import uuid
import textwrap
@ -16,7 +15,8 @@ from vdirsyncer.storage.base import normalize_meta_value
from vdirsyncer.vobject import Item
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
assert_item_equals, normalize_item, printable_characters_strategy
assert_item_equals, format_item, \
printable_characters_strategy
def get_server_mixin(server_name):
@ -25,12 +25,6 @@ def get_server_mixin(server_name):
return x.ServerMixin
def format_item(item_template, uid=None):
# assert that special chars are handled correctly.
r = random.random()
return Item(item_template.format(r=r, uid=uid or r))
class StorageTests(object):
storage_class = None
supports_collections = True
@ -62,7 +56,7 @@ class StorageTests(object):
'VCARD': VCARD_TEMPLATE,
}[item_type]
return lambda **kw: format_item(template, **kw)
return lambda **kw: format_item(item_template=template, **kw)
@pytest.fixture
def requires_collections(self):
@ -299,7 +293,7 @@ class StorageTests(object):
@given(value=st.one_of(
st.none(),
printable_characters_strategy.filter(lambda x: x.strip() != x)
printable_characters_strategy
))
def test_metadata_normalization(self, requires_metadata, s, value):
x = s.get_meta('displayname')
@ -354,4 +348,5 @@ class StorageTests(object):
href, etag = s.upload(item)
item2, etag2 = s.get(href)
assert normalize_item(item) == normalize_item(item2)
assert item2.raw.count('BEGIN:VEVENT') == 2
assert 'RRULE' in item2.raw

View file

@ -6,14 +6,8 @@ import os
import pytest
import requests
import requests.exceptions
from tests import assert_item_equals
from vdirsyncer import exceptions
from vdirsyncer.vobject import Item
from .. import StorageTests, get_server_mixin
@ -24,14 +18,6 @@ ServerMixin = get_server_mixin(dav_server)
class DAVStorageTests(ServerMixin, StorageTests):
dav_server = dav_server
@pytest.mark.skipif(dav_server == 'radicale',
reason='Radicale is very tolerant.')
def test_dav_broken_item(self, s):
item = Item(u'HAHA:YES')
with pytest.raises((exceptions.Error, requests.exceptions.HTTPError)):
s.upload(item)
assert not list(s.list())
def test_dav_empty_get_multi_performance(self, s, monkeypatch):
def breakdown(*a, **kw):
raise AssertionError('Expected not to be called.')

View file

@ -28,7 +28,7 @@ class TestCalDAVStorage(DAVStorageTests):
s = self.storage_class(item_types=(item_type,), **get_storage_args())
try:
s.upload(format_item(VCARD_TEMPLATE))
s.upload(format_item(item_template=VCARD_TEMPLATE))
except (exceptions.Error, requests.exceptions.HTTPError):
pass
assert not list(s.list())
@ -64,7 +64,7 @@ class TestCalDAVStorage(DAVStorageTests):
s = self.storage_class(start_date=start_date, end_date=end_date,
**get_storage_args())
too_old_item = format_item(dedent(u'''
too_old_item = format_item(item_template=dedent(u'''
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -78,7 +78,7 @@ class TestCalDAVStorage(DAVStorageTests):
END:VCALENDAR
''').strip())
too_new_item = format_item(dedent(u'''
too_new_item = format_item(item_template=dedent(u'''
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -92,7 +92,7 @@ class TestCalDAVStorage(DAVStorageTests):
END:VCALENDAR
''').strip())
good_item = format_item(dedent(u'''
good_item = format_item(item_template=dedent(u'''
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
@ -136,8 +136,8 @@ class TestCalDAVStorage(DAVStorageTests):
@pytest.mark.skipif(dav_server == 'icloud',
reason='iCloud only accepts VEVENT')
def test_item_types_general(self, s):
event = s.upload(format_item(EVENT_TEMPLATE))[0]
task = s.upload(format_item(TASK_TEMPLATE))[0]
event = s.upload(format_item(item_template=EVENT_TEMPLATE))[0]
task = s.upload(format_item(item_template=TASK_TEMPLATE))[0]
s.item_types = ('VTODO', 'VEVENT')
def l():

@ -1 +1 @@
Subproject commit a27144ddcf39a3283179a4f7ce1ab22b2e810205
Subproject commit 1427b0e8d6cf89c5876bce2dece3248822a2c147

@ -1 +1 @@
Subproject commit bb4fcc6f524467d58c95f1dcec8470fdfcd65adf
Subproject commit d3cfc453b0b2dded90c870fd23281fb0c13d766f

View file

@ -5,9 +5,9 @@ import subprocess
import pytest
from vdirsyncer.storage.filesystem import FilesystemStorage
from vdirsyncer.vobject import Item
from . import StorageTests
from tests import format_item
class TestFilesystemStorage(StorageTests):
@ -42,13 +42,13 @@ class TestFilesystemStorage(StorageTests):
def test_ident_with_slash(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt')
s.upload(Item(u'UID:a/b/c'))
s.upload(format_item('a/b/c'))
item_file, = tmpdir.listdir()
assert '/' not in item_file.basename and item_file.isfile()
def test_too_long_uid(self, tmpdir):
s = self.storage_class(str(tmpdir), '.txt')
item = Item(u'UID:' + u'hue' * 600)
item = format_item('hue' * 600)
href, etag = s.upload(item)
assert item.uid not in href
@ -60,7 +60,7 @@ class TestFilesystemStorage(StorageTests):
monkeypatch.setattr(subprocess, 'call', check_call_mock)
s = self.storage_class(str(tmpdir), '.txt', post_hook=None)
s.upload(Item(u'UID:a/b/c'))
s.upload(format_item('a/b/c'))
def test_post_hook_active(self, tmpdir, monkeypatch):
@ -75,7 +75,7 @@ class TestFilesystemStorage(StorageTests):
monkeypatch.setattr(subprocess, 'call', check_call_mock)
s = self.storage_class(str(tmpdir), '.txt', post_hook=exe)
s.upload(Item(u'UID:a/b/c'))
s.upload(format_item('a/b/c'))
assert calls
def test_ignore_git_dirs(self, tmpdir):

View file

@ -4,10 +4,9 @@ import pytest
from requests import Response
from tests import normalize_item
from vdirsyncer.exceptions import UserError
from vdirsyncer.storage.http import HttpStorage, prepare_auth
from vdirsyncer.vobject import Item
def test_list(monkeypatch):
@ -56,9 +55,9 @@ def test_list(monkeypatch):
item, etag2 = s.get(href)
assert item.uid is not None
assert etag2 == etag
found_items[normalize_item(item)] = href
found_items[item.hash] = href
expected = set(normalize_item(u'BEGIN:VCALENDAR\n' + x + '\nEND:VCALENDAR')
expected = set(Item(u'BEGIN:VCALENDAR\n' + x + '\nEND:VCALENDAR').hash
for x in items)
assert set(found_items) == expected
@ -67,7 +66,7 @@ def test_list(monkeypatch):
item, etag2 = s.get(href)
assert item.uid is not None
assert etag2 == etag
assert found_items[normalize_item(item)] == href
assert found_items[item.hash] == href
def test_readonly_param():

View file

@ -62,7 +62,7 @@ def test_repair_uids(storage, runner, repair_uids):
assert 'UID or href is unsafe, assigning random UID' in result.output
assert not f.exists()
new_f, = storage.listdir()
s = new_f.read()
s = new_f.read().strip()
assert s.startswith('BEGIN:VCARD')
assert s.endswith('END:VCARD')

View file

@ -9,6 +9,8 @@ from hypothesis import example, given
import pytest
from tests import format_item
def test_simple_run(tmpdir, runner):
runner.write_with_general(dedent('''
@ -37,10 +39,11 @@ def test_simple_run(tmpdir, runner):
result = runner.invoke(['sync'])
assert not result.exception
tmpdir.join('path_a/haha.txt').write('UID:haha')
item = format_item('haha')
tmpdir.join('path_a/haha.txt').write(item.raw)
result = runner.invoke(['sync'])
assert 'Copying (uploading) item haha to my_b' in result.output
assert tmpdir.join('path_b/haha.txt').read() == 'UID:haha'
assert tmpdir.join('path_b/haha.txt').read() == item.raw
def test_sync_inexistant_pair(tmpdir, runner):
@ -109,7 +112,8 @@ def test_empty_storage(tmpdir, runner):
result = runner.invoke(['sync'])
assert not result.exception
tmpdir.join('path_a/haha.txt').write('UID:haha')
item = format_item('haha')
tmpdir.join('path_a/haha.txt').write(item.raw)
result = runner.invoke(['sync'])
assert not result.exception
tmpdir.join('path_b/haha.txt').remove()
@ -152,7 +156,7 @@ def test_collections_cache_invalidation(tmpdir, runner):
collections = ["a", "b", "c"]
''').format(str(tmpdir)))
foo.join('a/itemone.txt').write('UID:itemone')
foo.join('a/itemone.txt').write(format_item('itemone').raw)
result = runner.invoke(['discover'])
assert not result.exception
@ -347,9 +351,10 @@ def test_ident_conflict(tmpdir, runner):
foo = tmpdir.mkdir('foo')
tmpdir.mkdir('bar')
foo.join('one.txt').write('UID:1')
foo.join('two.txt').write('UID:1')
foo.join('three.txt').write('UID:1')
item = format_item('1')
foo.join('one.txt').write(item.raw)
foo.join('two.txt').write(item.raw)
foo.join('three.txt').write(item.raw)
result = runner.invoke(['discover'])
assert not result.exception
@ -403,8 +408,12 @@ def test_no_configured_pairs(tmpdir, runner, cmd):
assert result.exception.code == 5
item_a = format_item('lol')
item_b = format_item('lol')
@pytest.mark.parametrize('resolution,expect_foo,expect_bar', [
(['command', 'cp'], 'UID:lol\nfööcontent', 'UID:lol\nfööcontent')
(['command', 'cp'], item_a.raw, item_a.raw)
])
def test_conflict_resolution(tmpdir, runner, resolution, expect_foo,
expect_bar):
@ -429,9 +438,9 @@ def test_conflict_resolution(tmpdir, runner, resolution, expect_foo,
foo = tmpdir.join('foo')
bar = tmpdir.join('bar')
fooitem = foo.join('lol.txt').ensure()
fooitem.write('UID:lol\nfööcontent')
fooitem.write(item_a.raw)
baritem = bar.join('lol.txt').ensure()
baritem.write('UID:lol\nbööcontent')
baritem.write(item_b.raw)
r = runner.invoke(['discover'])
assert not r.exception
@ -471,11 +480,12 @@ def test_partial_sync(tmpdir, runner, partial_sync):
foo = tmpdir.mkdir('foo')
bar = tmpdir.mkdir('bar')
foo.join('other.txt').write('UID:other')
bar.join('other.txt').write('UID:other')
item = format_item('other')
foo.join('other.txt').write(item.raw)
bar.join('other.txt').write(item.raw)
baritem = bar.join('lol.txt')
baritem.write('UID:lol')
baritem.write(format_item('lol').raw)
r = runner.invoke(['discover'])
assert not r.exception

View file

@ -8,7 +8,7 @@ from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
import pytest
from tests import blow_up, uid_strategy
from tests import blow_up, format_item, uid_strategy
from vdirsyncer.storage.memory import MemoryStorage, _random_string
from vdirsyncer.sync import sync as _sync
@ -49,7 +49,7 @@ def test_missing_status():
a = MemoryStorage()
b = MemoryStorage()
status = {}
item = Item(u'asdf')
item = format_item('asdf')
a.upload(item)
b.upload(item)
sync(a, b, status)
@ -62,8 +62,8 @@ def test_missing_status_and_different_items():
b = MemoryStorage()
status = {}
item1 = Item(u'UID:1\nhaha')
item2 = Item(u'UID:1\nhoho')
item1 = format_item('1')
item2 = format_item('1')
a.upload(item1)
b.upload(item2)
with pytest.raises(SyncConflict):
@ -79,8 +79,8 @@ def test_read_only_and_prefetch():
b.read_only = True
status = {}
item1 = Item(u'UID:1\nhaha')
item2 = Item(u'UID:2\nhoho')
item1 = format_item('1')
item2 = format_item('2')
a.upload(item1)
a.upload(item2)
@ -95,7 +95,8 @@ def test_partial_sync_error():
b = MemoryStorage()
status = {}
a.upload(Item('UID:0'))
item = format_item('0')
a.upload(item)
b.read_only = True
with pytest.raises(PartialSync):
@ -107,13 +108,13 @@ def test_partial_sync_ignore():
b = MemoryStorage()
status = {}
item0 = Item('UID:0\nhehe')
item0 = format_item('0')
a.upload(item0)
b.upload(item0)
b.read_only = True
item1 = Item('UID:1\nhaha')
item1 = format_item('1')
a.upload(item1)
sync(a, b, status, partial_sync='ignore')
@ -128,23 +129,25 @@ def test_partial_sync_ignore2():
b = MemoryStorage()
status = {}
href, etag = a.upload(Item('UID:0'))
item = format_item('0')
href, etag = a.upload(item)
a.read_only = True
sync(a, b, status, partial_sync='ignore', force_delete=True)
assert items(b) == items(a) == {'UID:0'}
assert items(b) == items(a) == {item.raw}
b.items.clear()
sync(a, b, status, partial_sync='ignore', force_delete=True)
sync(a, b, status, partial_sync='ignore', force_delete=True)
assert items(a) == {'UID:0'}
assert items(a) == {item.raw}
assert not b.items
a.read_only = False
a.update(href, Item('UID:0\nupdated'), etag)
new_item = format_item('0')
a.update(href, new_item, etag)
a.read_only = True
sync(a, b, status, partial_sync='ignore', force_delete=True)
assert items(b) == items(a) == {'UID:0\nupdated'}
assert items(b) == items(a) == {new_item.raw}
def test_upload_and_update():
@ -152,22 +155,22 @@ def test_upload_and_update():
b = MemoryStorage(fileext='.b')
status = {}
item = Item(u'UID:1') # new item 1 in a
item = format_item('1') # new item 1 in a
a.upload(item)
sync(a, b, status)
assert items(b) == items(a) == {item.raw}
item = Item(u'UID:1\nASDF:YES') # update of item 1 in b
item = format_item('1') # update of item 1 in b
b.update('1.b', item, b.get('1.b')[1])
sync(a, b, status)
assert items(b) == items(a) == {item.raw}
item2 = Item(u'UID:2') # new item 2 in b
item2 = format_item('2') # new item 2 in b
b.upload(item2)
sync(a, b, status)
assert items(b) == items(a) == {item.raw, item2.raw}
item2 = Item(u'UID:2\nASDF:YES') # update of item 2 in a
item2 = format_item('2') # update of item 2 in a
a.update('2.a', item2, a.get('2.a')[1])
sync(a, b, status)
assert items(b) == items(a) == {item.raw, item2.raw}
@ -178,9 +181,9 @@ def test_deletion():
b = MemoryStorage(fileext='.b')
status = {}
item = Item(u'UID:1')
item = format_item('1')
a.upload(item)
item2 = Item(u'UID:2')
item2 = format_item('2')
a.upload(item2)
sync(a, b, status)
b.delete('1.b', b.get('1.b')[1])
@ -200,14 +203,14 @@ def test_insert_hash():
b = MemoryStorage()
status = {}
item = Item('UID:1')
item = format_item('1')
href, etag = a.upload(item)
sync(a, b, status)
for d in status['1']:
del d['hash']
a.update(href, Item('UID:1\nHAHA:YES'), etag)
a.update(href, format_item('1'), etag) # new item content
sync(a, b, status)
assert 'hash' in status['1'][0] and 'hash' in status['1'][1]
@ -215,7 +218,7 @@ def test_insert_hash():
def test_already_synced():
a = MemoryStorage(fileext='.a')
b = MemoryStorage(fileext='.b')
item = Item(u'UID:1')
item = format_item('1')
a.upload(item)
b.upload(item)
status = {
@ -243,14 +246,14 @@ def test_already_synced():
def test_conflict_resolution_both_etags_new(winning_storage):
a = MemoryStorage()
b = MemoryStorage()
item = Item(u'UID:1')
item = format_item('1')
href_a, etag_a = a.upload(item)
href_b, etag_b = b.upload(item)
status = {}
sync(a, b, status)
assert status
item_a = Item(u'UID:1\nitem a')
item_b = Item(u'UID:1\nitem b')
item_a = format_item('1')
item_b = format_item('1')
a.update(href_a, item_a, etag_a)
b.update(href_b, item_b, etag_b)
with pytest.raises(SyncConflict):
@ -264,13 +267,14 @@ def test_conflict_resolution_both_etags_new(winning_storage):
def test_updated_and_deleted():
a = MemoryStorage()
b = MemoryStorage()
href_a, etag_a = a.upload(Item(u'UID:1'))
item = format_item('1')
href_a, etag_a = a.upload(item)
status = {}
sync(a, b, status, force_delete=True)
(href_b, etag_b), = b.list()
b.delete(href_b, etag_b)
updated = Item(u'UID:1\nupdated')
updated = format_item('1')
a.update(href_a, updated, etag_a)
sync(a, b, status, force_delete=True)
@ -280,8 +284,8 @@ def test_updated_and_deleted():
def test_conflict_resolution_invalid_mode():
a = MemoryStorage()
b = MemoryStorage()
item_a = Item(u'UID:1\nitem a')
item_b = Item(u'UID:1\nitem b')
item_a = format_item('1')
item_b = format_item('1')
a.upload(item_a)
b.upload(item_b)
with pytest.raises(ValueError):
@ -291,7 +295,7 @@ def test_conflict_resolution_invalid_mode():
def test_conflict_resolution_new_etags_without_changes():
a = MemoryStorage()
b = MemoryStorage()
item = Item(u'UID:1')
item = format_item('1')
href_a, etag_a = a.upload(item)
href_b, etag_b = b.upload(item)
status = {'1': (href_a, 'BOGUS_a', href_b, 'BOGUS_b')}
@ -326,7 +330,7 @@ def test_uses_get_multi(monkeypatch):
a = MemoryStorage()
b = MemoryStorage()
item = Item(u'UID:1')
item = format_item('1')
expected_href, etag = a.upload(item)
sync(a, b, {})
@ -336,8 +340,8 @@ def test_uses_get_multi(monkeypatch):
def test_empty_storage_dataloss():
a = MemoryStorage()
b = MemoryStorage()
a.upload(Item(u'UID:1'))
a.upload(Item(u'UID:2'))
for i in '12':
a.upload(format_item(i))
status = {}
sync(a, b, status)
with pytest.raises(StorageEmpty):
@ -350,22 +354,24 @@ def test_empty_storage_dataloss():
def test_no_uids():
a = MemoryStorage()
b = MemoryStorage()
a.upload(Item(u'ASDF'))
b.upload(Item(u'FOOBAR'))
item_a = format_item('')
item_b = format_item('')
a.upload(item_a)
b.upload(item_b)
status = {}
sync(a, b, status)
assert items(a) == items(b) == {u'ASDF', u'FOOBAR'}
assert items(a) == items(b) == {item_a.raw, item_b.raw}
def test_changed_uids():
a = MemoryStorage()
b = MemoryStorage()
href_a, etag_a = a.upload(Item(u'UID:A-ONE'))
href_b, etag_b = b.upload(Item(u'UID:B-ONE'))
href_a, etag_a = a.upload(format_item('a1'))
href_b, etag_b = b.upload(format_item('b1'))
status = {}
sync(a, b, status)
a.update(href_a, Item(u'UID:A-TWO'), etag_a)
a.update(href_a, format_item('a2'), etag_a)
sync(a, b, status)
@ -383,34 +389,37 @@ def test_partial_sync_revert():
a = MemoryStorage(instance_name='a')
b = MemoryStorage(instance_name='b')
status = {}
a.upload(Item(u'UID:1'))
b.upload(Item(u'UID:2'))
item1 = format_item('1')
item2 = format_item('2')
a.upload(item1)
b.upload(item2)
b.read_only = True
sync(a, b, status, partial_sync='revert')
assert len(status) == 2
assert items(a) == {'UID:1', 'UID:2'}
assert items(b) == {'UID:2'}
assert items(a) == {item1.raw, item2.raw}
assert items(b) == {item2.raw}
sync(a, b, status, partial_sync='revert')
assert len(status) == 1
assert items(a) == {'UID:2'}
assert items(b) == {'UID:2'}
assert items(a) == {item2.raw}
assert items(b) == {item2.raw}
# Check that updates get reverted
a.items[next(iter(a.items))] = ('foo', Item('UID:2\nupdated'))
assert items(a) == {'UID:2\nupdated'}
item2_up = format_item('2')
a.items[next(iter(a.items))] = ('foo', item2_up)
assert items(a) == {item2_up.raw}
sync(a, b, status, partial_sync='revert')
assert len(status) == 1
assert items(a) == {'UID:2\nupdated'}
assert items(a) == {item2_up.raw}
sync(a, b, status, partial_sync='revert')
assert items(a) == {'UID:2'}
assert items(a) == {item2.raw}
# Check that deletions get reverted
a.items.clear()
sync(a, b, status, partial_sync='revert', force_delete=True)
sync(a, b, status, partial_sync='revert', force_delete=True)
assert items(a) == {'UID:2'}
assert items(a) == {item2.raw}
@pytest.mark.parametrize('sync_inbetween', (True, False))
@ -418,13 +427,16 @@ def test_ident_conflict(sync_inbetween):
a = MemoryStorage()
b = MemoryStorage()
status = {}
href_a, etag_a = a.upload(Item(u'UID:aaa'))
href_b, etag_b = a.upload(Item(u'UID:bbb'))
item_a = format_item('aaa')
item_b = format_item('bbb')
href_a, etag_a = a.upload(item_a)
href_b, etag_b = a.upload(item_b)
if sync_inbetween:
sync(a, b, status)
a.update(href_a, Item(u'UID:xxx'), etag_a)
a.update(href_b, Item(u'UID:xxx'), etag_b)
item_x = format_item('xxx')
a.update(href_a, item_x, etag_a)
a.update(href_b, item_x, etag_b)
with pytest.raises(IdentConflict):
sync(a, b, status)
@ -441,7 +453,8 @@ def test_moved_href():
a = MemoryStorage()
b = MemoryStorage()
status = {}
href, etag = a.upload(Item(u'UID:haha'))
item = format_item('haha')
href, etag = a.upload(item)
sync(a, b, status)
b.items['lol'] = b.items.pop('haha')
@ -454,7 +467,7 @@ def test_moved_href():
sync(a, b, status)
assert len(status) == 1
assert items(a) == items(b) == {'UID:haha'}
assert items(a) == items(b) == {item.raw}
assert status['haha'][1]['href'] == 'lol'
old_status = deepcopy(status)
@ -463,7 +476,7 @@ def test_moved_href():
sync(a, b, status)
assert old_status == status
assert items(a) == items(b) == {'UID:haha'}
assert items(a) == items(b) == {item.raw}
def test_bogus_etag_change():
@ -476,26 +489,31 @@ def test_bogus_etag_change():
a = MemoryStorage()
b = MemoryStorage()
status = {}
href_a, etag_a = a.upload(Item(u'UID:ASDASD'))
sync(a, b, status)
assert len(status) == len(list(a.list())) == len(list(b.list())) == 1
item = format_item('ASDASD')
href_a, etag_a = a.upload(item)
sync(a, b, status)
assert len(status) == 1
assert items(a) == items(b) == {item.raw}
new_item = format_item('ASDASD')
(href_b, etag_b), = b.list()
a.update(href_a, Item(u'UID:ASDASD'), etag_a)
b.update(href_b, Item(u'UID:ASDASD\nACTUALCHANGE:YES'), etag_b)
a.update(href_a, item, etag_a)
b.update(href_b, new_item, etag_b)
b.delete = b.update = b.upload = blow_up
sync(a, b, status)
assert len(status) == 1
assert items(a) == items(b) == {u'UID:ASDASD\nACTUALCHANGE:YES'}
assert items(a) == items(b) == {new_item.raw}
def test_unicode_hrefs():
a = MemoryStorage()
b = MemoryStorage()
status = {}
href, etag = a.upload(Item(u'UID:äää'))
item = format_item('äää')
href, etag = a.upload(item)
sync(a, b, status)
@ -565,7 +583,7 @@ class SyncMachine(RuleBasedStateMachine):
uid=uid_strategy,
etag=st.text())
def upload(self, storage, uid, etag):
item = Item(u'UID:{}'.format(uid))
item = Item('BEGIN:VCARD\r\nUID:{}\r\nEND:VCARD'.format(uid))
storage.items[uid] = (etag, item)
@rule(storage=Storage, href=st.text())
@ -643,8 +661,8 @@ def test_rollback(error_callback):
b = MemoryStorage()
status = {}
a.items['0'] = ('', Item('UID:0'))
b.items['1'] = ('', Item('UID:1'))
a.items['0'] = ('', format_item('0'))
b.items['1'] = ('', format_item('1'))
b.upload = b.update = b.delete = action_failure
@ -668,7 +686,7 @@ def test_duplicate_hrefs():
a = MemoryStorage()
b = MemoryStorage()
a.list = lambda: [('a', 'a')] * 3
a.items['a'] = ('a', Item('UID:a'))
a.items['a'] = ('a', format_item('a'))
status = {}
sync(a, b, status)

View file

@ -38,7 +38,7 @@ def test_repair_uids(uid):
@settings(perform_health_check=False) # Using the random module for UIDs
def test_repair_unsafe_uids(uid):
s = MemoryStorage()
item = Item(u'BEGIN:VCARD\nUID:{}\nEND:VCARD'.format(uid))
item = Item(u'BEGIN:VCARD\nUID:123\nEND:VCARD').with_uid(uid)
href, etag = s.upload(item)
assert s.get(href)[0].uid == uid
assert not href_safe(uid)

View file

@ -9,7 +9,7 @@ from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule
import pytest
from tests import BARE_EVENT_TEMPLATE, EVENT_TEMPLATE, \
EVENT_WITH_TIMEZONE_TEMPLATE, VCARD_TEMPLATE, normalize_item, \
EVENT_WITH_TIMEZONE_TEMPLATE, VCARD_TEMPLATE, \
uid_strategy
import vdirsyncer.vobject as vobject
@ -31,8 +31,8 @@ _simple_joined = u'\r\n'.join(
def test_split_collection_simple(benchmark):
given = benchmark(lambda: list(vobject.split_collection(_simple_joined)))
assert [normalize_item(item) for item in given] == \
[normalize_item(item) for item in _simple_split]
assert [vobject.Item(item).hash for item in given] == \
[vobject.Item(item).hash for item in _simple_split]
assert [x.splitlines() for x in given] == \
[x.splitlines() for x in _simple_split]
@ -47,8 +47,8 @@ def test_split_collection_multiple_wrappers(benchmark):
)
given = benchmark(lambda: list(vobject.split_collection(joined)))
assert [normalize_item(item) for item in given] == \
[normalize_item(item) for item in _simple_split]
assert [vobject.Item(item).hash for item in given] == \
[vobject.Item(item).hash for item in _simple_split]
assert [x.splitlines() for x in given] == \
[x.splitlines() for x in _simple_split]
@ -56,7 +56,7 @@ def test_split_collection_multiple_wrappers(benchmark):
def test_join_collection_simple(benchmark):
given = benchmark(lambda: vobject.join_collection(_simple_split))
assert normalize_item(given) == normalize_item(_simple_joined)
assert vobject.Item(given).hash == vobject.Item(_simple_joined).hash
assert given.splitlines() == _simple_joined.splitlines()
@ -123,12 +123,12 @@ def test_split_collection_timezones():
[timezone, u'END:VCALENDAR']
)
given = set(normalize_item(item)
given = set(vobject.Item(item).hash
for item in vobject.split_collection(full))
expected = set(
normalize_item(u'\r\n'.join((
vobject.Item(u'\r\n'.join((
u'BEGIN:VCALENDAR', item, timezone, u'END:VCALENDAR'
)))
))).hash
for item in items
)
@ -146,11 +146,11 @@ def test_split_contacts():
with_wrapper.splitlines()
def test_hash_item():
def test_hash_item2():
a = EVENT_TEMPLATE.format(r=1, uid=1)
b = u'\n'.join(line for line in a.splitlines()
if u'PRODID' not in line)
assert vobject.hash_item(a) == vobject.hash_item(b)
assert vobject.Item(a).hash == vobject.Item(b).hash
def test_multiline_uid(benchmark):
@ -223,7 +223,7 @@ def test_replace_uid(template, uid):
item = vobject.Item(template.format(r=123, uid=123)).with_uid(uid)
assert item.uid == uid
if uid:
assert item.raw.count('\nUID:{}'.format(uid)) == 1
assert item.raw.count('\nUID:') == 1
else:
assert '\nUID:' not in item.raw
@ -351,3 +351,47 @@ def test_component_contains():
with pytest.raises(ValueError):
42 in item
def test_hash_item():
item1 = vobject.Item(
'BEGIN:FOO\r\n'
'X-RADICALE-NAME:YES\r\n'
'END:FOO\r\n'
)
item2 = vobject.Item(
'BEGIN:FOO\r\n'
'X-RADICALE-NAME:NO\r\n'
'END:FOO\r\n'
)
assert item1.hash == item2.hash
item2 = vobject.Item(
'BEGIN:FOO\r\n'
'X-RADICALE-NAME:NO\r\n'
'OTHER-PROP:YAY\r\n'
'END:FOO\r\n'
)
assert item1.hash != item2.hash
def test_hash_item_timezones():
item1 = vobject.Item(
'BEGIN:VCALENDAR\r\n'
'HELLO:HAHA\r\n'
'BEGIN:VTIMEZONE\r\n'
'PROP:YES\r\n'
'END:VTIMEZONE\r\n'
'END:VCALENDAR\r\n'
)
item2 = vobject.Item(
'BEGIN:VCALENDAR\r\n'
'HELLO:HAHA\r\n'
'END:VCALENDAR\r\n'
)
assert item1.hash == item2.hash

View file

@ -79,3 +79,7 @@ class UnsupportedMetadataError(Error, NotImplementedError):
class CollectionRequired(Error):
'''`collection = null` is not allowed.'''
class VobjectParseError(Error, ValueError):
'''The parsed vobject is invalid.'''

45
vdirsyncer/native.py Normal file
View file

@ -0,0 +1,45 @@
from ._native import ffi, lib
from .exceptions import VobjectParseError
def parse_component(raw):
e = ffi.new('VdirsyncerError *')
try:
c = lib.vdirsyncer_parse_component(raw, e)
if e.failed:
raise VobjectParseError(ffi.string(e.msg).decode('utf-8'))
return _component_rv(c)
finally:
if e.failed:
lib.vdirsyncer_clear_err(e)
def write_component(component):
return _string_rv(lib.vdirsyncer_write_component(component))
def get_uid(component):
return _string_rv(lib.vdirsyncer_get_uid(component))
def _string_rv(c_str):
try:
return ffi.string(c_str).decode('utf-8')
finally:
lib.vdirsyncer_free_str(c_str)
def change_uid(component, uid):
lib.vdirsyncer_change_uid(component, uid.encode('utf-8'))
def _component_rv(c):
return ffi.gc(c, lib.vdirsyncer_free_component)
def clone_component(c):
return _component_rv(lib.vdirsyncer_clone_component(c))
def hash_component(c):
return _string_rv(lib.vdirsyncer_hash_component(c))

View file

@ -146,7 +146,7 @@ class Discover(object):
_homeset_xml = None
_homeset_tag = None
_well_known_uri = None
_collection_xml = b"""
_collection_xml = b"""<?xml version="1.0" encoding="utf-8" ?>
<d:propfind xmlns:d="DAV:">
<d:prop>
<d:resourcetype />
@ -724,8 +724,8 @@ class CalDAVStorage(DAVStorage):
example, the following would synchronize the timerange from one year in the
past to one year in the future::
start_date = datetime.now() - timedelta(days=365)
end_date = datetime.now() + timedelta(days=365)
start_date = "datetime.now() - timedelta(days=365)"
end_date = "datetime.now() + timedelta(days=365)"
Either both or none have to be specified. The default is to synchronize
everything.
@ -792,32 +792,28 @@ class CalDAVStorage(DAVStorage):
@staticmethod
def _get_list_filters(components, start, end):
if components:
caldavfilter = '''
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="{component}">
{timefilter}
</C:comp-filter>
caldavfilter = '''
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="{component}">
{timefilter}
</C:comp-filter>
'''
</C:comp-filter>
'''
if start is not None and end is not None:
start = start.strftime(CALDAV_DT_FORMAT)
end = end.strftime(CALDAV_DT_FORMAT)
timefilter = ''
timefilter = ('<C:time-range start="{start}" end="{end}"/>'
.format(start=start, end=end))
else:
timefilter = ''
if start is not None and end is not None:
start = start.strftime(CALDAV_DT_FORMAT)
end = end.strftime(CALDAV_DT_FORMAT)
for component in components:
yield caldavfilter.format(component=component,
timefilter=timefilter)
else:
if start is not None and end is not None:
for x in CalDAVStorage._get_list_filters(('VTODO', 'VEVENT'),
start, end):
yield x
timefilter = ('<C:time-range start="{start}" end="{end}"/>'
.format(start=start, end=end))
if not components:
components = ('VTODO', 'VEVENT')
for component in components:
yield caldavfilter.format(component=component,
timefilter=timefilter)
def list(self):
caldavfilters = list(self._get_list_filters(
@ -833,8 +829,8 @@ class CalDAVStorage(DAVStorage):
# instead?
#
# See https://github.com/dmfs/tasks/issues/118 for backstory.
for x in DAVStorage.list(self):
yield x
yield from DAVStorage.list(self)
return
data = '''<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"

View file

@ -1,37 +1,9 @@
# -*- coding: utf-8 -*-
import hashlib
from itertools import chain, tee
from .utils import cached_property, uniq
IGNORE_PROPS = (
# PRODID is changed by radicale for some reason after upload
'PRODID',
# Sometimes METHOD:PUBLISH is added by WebCAL providers, for us it doesn't
# make a difference
'METHOD',
# X-RADICALE-NAME is used by radicale, because hrefs don't really exist in
# their filesystem backend
'X-RADICALE-NAME',
# Apparently this is set by Horde?
# https://github.com/pimutils/vdirsyncer/issues/318
'X-WR-CALNAME',
# Those are from the VCARD specification and is supposed to change when the
# item does -- however, we can determine that ourselves
'REV',
'LAST-MODIFIED',
'CREATED',
# Some iCalendar HTTP calendars generate the DTSTAMP at request time, so
# this property always changes when the rest of the item didn't. Some do
# the same with the UID.
#
# - Google's read-only calendar links
# - http://www.feiertage-oesterreich.at/
'DTSTAMP',
'UID',
)
from . import exceptions, native
class Item(object):
@ -39,25 +11,28 @@ class Item(object):
'''Immutable wrapper class for VCALENDAR (VEVENT, VTODO) and
VCARD'''
def __init__(self, raw):
def __init__(self, raw, component=None):
assert isinstance(raw, str), type(raw)
self._raw = raw
def with_uid(self, new_uid):
parsed = _Component.parse(self.raw)
stack = [parsed]
while stack:
component = stack.pop()
stack.extend(component.subcomponents)
if component.name in ('VEVENT', 'VTODO', 'VJOURNAL', 'VCARD'):
del component['UID']
if new_uid:
component['UID'] = new_uid
return Item('\r\n'.join(parsed.dump_lines()))
if component is not None:
self.__dict__['_component'] = component
@cached_property
def _component(self):
try:
return native.parse_component(self.raw.encode('utf-8'))
except exceptions.VobjectParseError:
return None
def with_uid(self, new_uid):
if not self._component:
raise ValueError('Item malformed.')
new_c = native.clone_component(self._component)
native.change_uid(new_c, new_uid or '')
return Item(native.write_component(new_c), component=new_c)
@property
def raw(self):
'''Raw content of the item, as unicode string.
@ -69,18 +44,17 @@ class Item(object):
def uid(self):
'''Global identifier of the item, across storages, doesn't change after
a modification of the item.'''
# Don't actually parse component, but treat all lines as single
# component, avoiding traversal through all subcomponents.
x = _Component('TEMP', self.raw.splitlines(), [])
try:
return x['UID'].strip() or None
except KeyError:
if not self._component:
return None
return native.get_uid(self._component) or None
@cached_property
def hash(self):
'''Hash of self.raw, used for etags.'''
return hash_item(self.raw)
'''Used for etags.'''
if not self.is_valid:
raise ValueError('Item malformed.')
return native.hash_component(self._component)
@cached_property
def ident(self):
@ -99,40 +73,15 @@ class Item(object):
@property
def parsed(self):
'''Don't cache because the rv is mutable.'''
# FIXME: remove
try:
return _Component.parse(self.raw)
except Exception:
return None
def normalize_item(item, ignore_props=IGNORE_PROPS):
'''Create syntactically invalid mess that is equal for similar items.'''
if not isinstance(item, Item):
item = Item(item)
item = _strip_timezones(item)
x = _Component('TEMP', item.raw.splitlines(), [])
for prop in IGNORE_PROPS:
del x[prop]
x.props.sort()
return u'\r\n'.join(filter(bool, (line.strip() for line in x.props)))
def _strip_timezones(item):
parsed = item.parsed
if not parsed or parsed.name != 'VCALENDAR':
return item
parsed.subcomponents = [c for c in parsed.subcomponents
if c.name != 'VTIMEZONE']
return Item('\r\n'.join(parsed.dump_lines()))
def hash_item(text):
return hashlib.sha256(normalize_item(text).encode('utf-8')).hexdigest()
@property
def is_valid(self):
return bool(self._component)
def split_collection(text):