├── .gitignore ├── tests ├── .gitignore ├── dummy_home │ ├── plug │ │ ├── .config │ │ │ └── nvim │ │ ├── .gitignore │ │ └── vim-coiled-snake │ │ │ ├── plugin │ │ │ ├── autoload │ │ │ └── ftplugin │ └── native │ │ ├── .gitignore │ │ └── .vim │ │ ├── vimrc │ │ └── pack │ │ └── dev │ │ └── start │ │ └── vim-coiled-snake │ │ ├── plugin │ │ ├── autoload │ │ └── ftplugin ├── speed │ ├── .gitignore │ ├── profile.sh │ └── small_folds.py ├── corner_cases │ ├── no_indent.py │ ├── small_indent.py │ ├── fold_in_comment.py │ ├── unindented_function.py │ ├── one_line_docstring.py │ ├── comment_dedent_2.py │ ├── line_continuation.py │ ├── fold_in_string.py │ ├── extra_indent.py │ ├── one_line_class.py │ ├── paren_between_strs.py │ ├── indented_struct.py │ ├── complex_parens.py │ ├── multiline_str_with_paren.py │ ├── comment_dedent.py │ ├── inline_list_addition.py │ ├── comment_after_function.py │ └── quoted_paren.py ├── issues │ ├── dummy_repo_16 │ │ ├── .gitignore │ │ ├── git │ │ │ ├── HEAD │ │ │ ├── COMMIT_EDITMSG │ │ │ ├── refs │ │ │ │ └── heads │ │ │ │ │ └── master │ │ │ ├── description │ │ │ ├── index │ │ │ ├── config │ │ │ ├── logs │ │ │ │ ├── HEAD │ │ │ │ └── refs │ │ │ │ │ └── heads │ │ │ │ │ └── master │ │ │ ├── objects │ │ │ │ ├── 10 │ │ │ │ │ └── fa14c5ab0134436e2ae435138bf921eb477c60 │ │ │ │ ├── 14 │ │ │ │ │ └── 12bf0ef750605b9a8887dc7fcee1e799cec540 │ │ │ │ ├── 39 │ │ │ │ │ └── 9eab1924e39da570b389b0bef1ca713b3b05c3 │ │ │ │ ├── 41 │ │ │ │ │ └── 42082bcb939bbc17985a69ba748491ac6b62a5 │ │ │ │ ├── 49 │ │ │ │ │ └── 8b267a8c7812490d6479839c5577eaaec79d62 │ │ │ │ ├── 51 │ │ │ │ │ └── 5f4836297fdf7567c066983c16e5eff598f7bd │ │ │ │ ├── 58 │ │ │ │ │ └── 52f44639f52db67d30ad9143b86afb143d415f │ │ │ │ ├── 59 │ │ │ │ │ └── a57ca05dbd6697b5df4ac4b4fe2ee83f58ded8 │ │ │ │ ├── 61 │ │ │ │ │ └── 87dbf4390fc6e28445dd3d988aefb9d1111988 │ │ │ │ ├── 72 │ │ │ │ │ └── f3b798bbbb26acba28d5330fe9eebe06debe34 │ │ │ │ ├── 77 │ │ │ │ │ └── 4ca4a32281246805d4f37ae5e04899c7fd0586 │ │ │ │ ├── 80 │ │ │ │ │ └── ba94135cc378364af9d3cb2450df48e51faf2c │ │ │ │ ├── 1a │ │ │ │ │ └── bb2b3223eb8cf97eec3b501ac50ff01b5bd7bd │ │ │ │ ├── 6a │ │ │ │ │ └── 756416384c210ada2631f17862f5c01fffa478 │ │ │ │ ├── 6b │ │ │ │ │ └── 8710a711f3b689885aa5c26c6c06bde348e82b │ │ │ │ ├── 6c │ │ │ │ │ └── bef5c370d8c3486ca85423dd70440c5e0a2aa2 │ │ │ │ ├── a1 │ │ │ │ │ └── fd29ec14823d8bc4a8d1a2cfe35451580f5118 │ │ │ │ ├── a5 │ │ │ │ │ ├── 196d1be8fb59edf8062bef36d3a602e0812139 │ │ │ │ │ └── d7b84a673458d14d9aab082183a1968c2c7492 │ │ │ │ ├── a9 │ │ │ │ │ └── ab4b760fac7cb80cf8acedd61450a28b995f25 │ │ │ │ ├── b5 │ │ │ │ │ ├── 5153268dfe9798446fba79370025190d6e7854 │ │ │ │ │ └── 8d1184a9d43a39c0d95f32453efc78581877d6 │ │ │ │ ├── c2 │ │ │ │ │ └── 39ea693db9eb39c2a8f54e63f4791c6f1871cc │ │ │ │ ├── c3 │ │ │ │ │ └── 8c8524069788e28ebc7d850d0ced46909b4cd0 │ │ │ │ ├── cb │ │ │ │ │ ├── 089cd89a7d7686d284d8761201649346b5aa1c │ │ │ │ │ └── 539f07601817f27a4bd1683a645071473a9f85 │ │ │ │ ├── e6 │ │ │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ │ │ ├── ea │ │ │ │ │ └── 57c1462692c22c42cd8a15a7191808ea8cd947 │ │ │ │ ├── ec │ │ │ │ │ └── 17ec1939b7c3e86b7cb6c0c4de6b0818a7e75e │ │ │ │ └── ef │ │ │ │ │ └── 94fa293800b31e3982204f43b13785afebd9bd │ │ │ ├── hooks │ │ │ │ ├── post-update.sample │ │ │ │ ├── pre-merge-commit.sample │ │ │ │ ├── pre-applypatch.sample │ │ │ │ ├── applypatch-msg.sample │ │ │ │ ├── pre-receive.sample │ │ │ │ ├── commit-msg.sample │ │ │ │ ├── pre-push.sample │ │ │ │ ├── prepare-commit-msg.sample │ │ │ │ ├── pre-commit.sample │ │ │ │ ├── fsmonitor-watchman.sample │ │ │ │ ├── update.sample │ │ │ │ └── pre-rebase.sample │ │ │ └── info │ │ │ │ └── exclude │ │ ├── run_vim_16.sh │ │ └── test_16.py │ ├── dummy_home_16 │ │ ├── .gitignore │ │ └── .vim │ │ │ ├── pack │ │ │ └── dev │ │ │ │ └── start │ │ │ │ └── vim-coiled-snake │ │ │ │ ├── autoload │ │ │ │ └── ftplugin │ │ │ └── vimrc │ ├── test_6.py │ ├── vimrc_3 │ ├── test_21.py │ ├── test_29.py │ ├── test_7.py │ ├── test_4.py │ ├── vimrc_7 │ ├── test_8.py │ ├── test_12.py │ ├── vimrc_2 │ └── test_3.py ├── easy │ ├── function.py │ ├── decorator.py │ ├── imports.py │ ├── docstring.py │ ├── class.py │ └── data_struct.py ├── vimrc_wo_flags ├── vimrc_wo_foldexpr ├── vimrc_wo_foldtext ├── test_ignore.py ├── run_vim_native.sh ├── run_vim.sh ├── run_nvim.sh ├── README ├── vimrc_simpylfold ├── vimrc ├── vimrc_explicit_sign_width ├── test_label.py ├── test_decorators.py ├── test_blocks.py ├── test_nested.py ├── test_everything.py ├── test_docstring.py ├── vimrc_config_fold ├── test_data_structs.py └── test_imports.py ├── IDEAS.md ├── ftplugin └── python │ └── coiledsnake.vim ├── plugin └── coiledsnake.vim ├── README.md └── autoload └── coiledsnake.vim /.gitignore: -------------------------------------------------------------------------------- 1 | images 2 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | test_sandbox.py 2 | -------------------------------------------------------------------------------- /tests/dummy_home/plug/.config/nvim: -------------------------------------------------------------------------------- 1 | ../.vim -------------------------------------------------------------------------------- /tests/speed/.gitignore: -------------------------------------------------------------------------------- 1 | coiledsnake.prof 2 | -------------------------------------------------------------------------------- /tests/corner_cases/no_indent.py: -------------------------------------------------------------------------------- 1 | import this 2 | -------------------------------------------------------------------------------- /tests/dummy_home/native/.gitignore: -------------------------------------------------------------------------------- 1 | .viminfo 2 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /tests/easy/function.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests/issues/dummy_home_16/.gitignore: -------------------------------------------------------------------------------- 1 | .viminfo 2 | -------------------------------------------------------------------------------- /tests/corner_cases/small_indent.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | pass 3 | -------------------------------------------------------------------------------- /tests/dummy_home/plug/.gitignore: -------------------------------------------------------------------------------- 1 | /.vim/plugged 2 | /.local 3 | -------------------------------------------------------------------------------- /tests/dummy_home/plug/vim-coiled-snake/plugin: -------------------------------------------------------------------------------- 1 | ../../../../plugin -------------------------------------------------------------------------------- /tests/easy/decorator.py: -------------------------------------------------------------------------------- 1 | @bar 2 | def foo(): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /tests/corner_cases/fold_in_comment.py: -------------------------------------------------------------------------------- 1 | # def foo() 2 | # pass 3 | -------------------------------------------------------------------------------- /tests/corner_cases/unindented_function.py: -------------------------------------------------------------------------------- 1 | def foo 2 | code = 1 3 | -------------------------------------------------------------------------------- /tests/dummy_home/plug/vim-coiled-snake/autoload: -------------------------------------------------------------------------------- 1 | ../../../../autoload -------------------------------------------------------------------------------- /tests/dummy_home/plug/vim-coiled-snake/ftplugin: -------------------------------------------------------------------------------- 1 | ../../../../ftplugin -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/COMMIT_EDITMSG: -------------------------------------------------------------------------------- 1 | Initial commit 2 | -------------------------------------------------------------------------------- /tests/dummy_home/native/.vim/vimrc: -------------------------------------------------------------------------------- 1 | filetype plugin indent on 2 | 3 | -------------------------------------------------------------------------------- /tests/easy/imports.py: -------------------------------------------------------------------------------- 1 | import foo 2 | import foo 3 | import foo 4 | import foo 5 | -------------------------------------------------------------------------------- /tests/vimrc_wo_flags: -------------------------------------------------------------------------------- 1 | source vimrc 2 | 3 | let g:coiled_snake_foldtext_flags = [] 4 | -------------------------------------------------------------------------------- /tests/vimrc_wo_foldexpr: -------------------------------------------------------------------------------- 1 | source vimrc 2 | 3 | let g:coiled_snake_set_foldexpr = 0 4 | -------------------------------------------------------------------------------- /tests/corner_cases/one_line_docstring.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | """Docstring""" 3 | pass 4 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/refs/heads/master: -------------------------------------------------------------------------------- 1 | c239ea693db9eb39c2a8f54e63f4791c6f1871cc 2 | -------------------------------------------------------------------------------- /tests/vimrc_wo_foldtext: -------------------------------------------------------------------------------- 1 | source vimrc 2 | 3 | let g:coiled_snake_set_foldtext = 0 4 | 5 | -------------------------------------------------------------------------------- /tests/corner_cases/comment_dedent_2.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | a = 1 3 | # comment 4 | b = 2 5 | -------------------------------------------------------------------------------- /tests/corner_cases/line_continuation.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | x = 1 + \ 3 | 2 4 | 5 | bar = 3 6 | -------------------------------------------------------------------------------- /tests/dummy_home/native/.vim/pack/dev/start/vim-coiled-snake/plugin: -------------------------------------------------------------------------------- 1 | ../../../../../../../../plugin -------------------------------------------------------------------------------- /tests/corner_cases/fold_in_string.py: -------------------------------------------------------------------------------- 1 | """ 2 | Docstring 3 | 4 | def foo(): 5 | pass 6 | """ 7 | -------------------------------------------------------------------------------- /tests/dummy_home/native/.vim/pack/dev/start/vim-coiled-snake/autoload: -------------------------------------------------------------------------------- 1 | ../../../../../../../../autoload -------------------------------------------------------------------------------- /tests/dummy_home/native/.vim/pack/dev/start/vim-coiled-snake/ftplugin: -------------------------------------------------------------------------------- 1 | ../../../../../../../../ftplugin -------------------------------------------------------------------------------- /tests/easy/docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Foo 3 | """ 4 | 5 | """ Bar """ 6 | 7 | def bax(): 8 | pass 9 | -------------------------------------------------------------------------------- /tests/test_ignore.py: -------------------------------------------------------------------------------- 1 | def foo(): # 2 | pass 3 | 4 | @decorator 5 | def foo(): # 6 | pass 7 | -------------------------------------------------------------------------------- /tests/corner_cases/extra_indent.py: -------------------------------------------------------------------------------- 1 | for x in xs: 2 | def foo(): 3 | pass 4 | code = 1 5 | -------------------------------------------------------------------------------- /tests/corner_cases/one_line_class.py: -------------------------------------------------------------------------------- 1 | # No folds expected. 2 | class A: pass 3 | class B: pass 4 | c = 1 5 | -------------------------------------------------------------------------------- /tests/issues/dummy_home_16/.vim/pack/dev/start/vim-coiled-snake/autoload: -------------------------------------------------------------------------------- 1 | ../../../../../../../../autoload -------------------------------------------------------------------------------- /tests/issues/dummy_home_16/.vim/pack/dev/start/vim-coiled-snake/ftplugin: -------------------------------------------------------------------------------- 1 | ../../../../../../../../ftplugin -------------------------------------------------------------------------------- /tests/run_vim_native.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | DIR=$(dirname $0) 3 | HOME=$DIR/dummy_home/native vim "$@" 4 | -------------------------------------------------------------------------------- /tests/run_vim.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | DIR=$(dirname $0) 3 | HOME=$DIR/dummy_home/plug vim -u $DIR/vimrc "$@" 4 | -------------------------------------------------------------------------------- /tests/easy/class.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | 3 | def foo(): 4 | pass 5 | 6 | def bar(): 7 | pass 8 | -------------------------------------------------------------------------------- /tests/run_nvim.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | DIR=$(dirname $0) 3 | HOME=$DIR/dummy_home/plug nvim -u $DIR/vimrc "$@" 4 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/run_vim_16.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ln -sf git .git 4 | HOME=../dummy_home_16 vim test_16.py 5 | -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | Before running any tests, initialize the blank runtime by doing the following: 2 | 3 | ./run_vim.sh 4 | :PlugInstall 5 | -------------------------------------------------------------------------------- /tests/issues/dummy_home_16/.vim/vimrc: -------------------------------------------------------------------------------- 1 | filetype plugin indent on 2 | 3 | " Recommended for vim-gitgutter 4 | set updatetime=100 5 | 6 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/index -------------------------------------------------------------------------------- /tests/issues/test_6.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | async def read_data(db): 4 | pass 5 | 6 | def normal_func(db): 7 | pass 8 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | -------------------------------------------------------------------------------- /IDEAS.md: -------------------------------------------------------------------------------- 1 | - Sit in the background and use some library to parse python code. See: parso 2 | 3 | https://parso.readthedocs.io/en/latest/docs/usage.html 4 | -------------------------------------------------------------------------------- /tests/speed/profile.sh: -------------------------------------------------------------------------------- 1 | nvim \ 2 | --cmd 'profile start coiledsnake.prof' \ 3 | --cmd 'profile! file ../../autoload/coiledsnake.vim' \ 4 | small_folds.py 5 | -------------------------------------------------------------------------------- /tests/issues/vimrc_3: -------------------------------------------------------------------------------- 1 | call plug#begin('~/.vim/plugged') 2 | Plug '~/hacking/projects/vim-coiled-snake' 3 | Plug 'Konfekt/FastFold' 4 | call plug#end() 5 | 6 | set number 7 | -------------------------------------------------------------------------------- /tests/vimrc_simpylfold: -------------------------------------------------------------------------------- 1 | call plug#begin() 2 | Plug 'tmhedberg/SimpylFold' 3 | Plug 'Konfekt/FastFold' 4 | call plug#end() 5 | 6 | set foldcolumn=5 7 | set number 8 | 9 | -------------------------------------------------------------------------------- /tests/corner_cases/paren_between_strs.py: -------------------------------------------------------------------------------- 1 | # The parenthesis after "a" should end the data structure, allowing `bar()` to 2 | # fold. 3 | foo( 4 | "a" ), "b" 5 | 6 | def bar(): 7 | pass 8 | -------------------------------------------------------------------------------- /tests/corner_cases/indented_struct.py: -------------------------------------------------------------------------------- 1 | # This list shouldn't be folded, since it's indented and shorter than 6 lines. 2 | 3 | list = [ 4 | 1, 5 | 2, 6 | 3, 7 | ] 8 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 c239ea693db9eb39c2a8f54e63f4791c6f1871cc Kale Kundert 1583684496 -0400 commit (initial): Initial commit 2 | -------------------------------------------------------------------------------- /tests/vimrc: -------------------------------------------------------------------------------- 1 | set cpoptions-=C 2 | 3 | call plug#begin() 4 | Plug '~/vim-coiled-snake' 5 | Plug 'Konfekt/FastFold' 6 | call plug#end() 7 | 8 | set foldcolumn=5 9 | set signcolumn=yes 10 | set number 11 | 12 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 c239ea693db9eb39c2a8f54e63f4791c6f1871cc Kale Kundert 1583684496 -0400 commit (initial): Initial commit 2 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/10/fa14c5ab0134436e2ae435138bf921eb477c60: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/10/fa14c5ab0134436e2ae435138bf921eb477c60 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/14/12bf0ef750605b9a8887dc7fcee1e799cec540: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/14/12bf0ef750605b9a8887dc7fcee1e799cec540 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/1a/bb2b3223eb8cf97eec3b501ac50ff01b5bd7bd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/1a/bb2b3223eb8cf97eec3b501ac50ff01b5bd7bd -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/39/9eab1924e39da570b389b0bef1ca713b3b05c3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/39/9eab1924e39da570b389b0bef1ca713b3b05c3 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/41/42082bcb939bbc17985a69ba748491ac6b62a5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/41/42082bcb939bbc17985a69ba748491ac6b62a5 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/49/8b267a8c7812490d6479839c5577eaaec79d62: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/49/8b267a8c7812490d6479839c5577eaaec79d62 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/51/5f4836297fdf7567c066983c16e5eff598f7bd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/51/5f4836297fdf7567c066983c16e5eff598f7bd -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/58/52f44639f52db67d30ad9143b86afb143d415f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/58/52f44639f52db67d30ad9143b86afb143d415f -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/59/a57ca05dbd6697b5df4ac4b4fe2ee83f58ded8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/59/a57ca05dbd6697b5df4ac4b4fe2ee83f58ded8 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/61/87dbf4390fc6e28445dd3d988aefb9d1111988: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/61/87dbf4390fc6e28445dd3d988aefb9d1111988 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/6a/756416384c210ada2631f17862f5c01fffa478: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/6a/756416384c210ada2631f17862f5c01fffa478 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/6b/8710a711f3b689885aa5c26c6c06bde348e82b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/6b/8710a711f3b689885aa5c26c6c06bde348e82b -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/6c/bef5c370d8c3486ca85423dd70440c5e0a2aa2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/6c/bef5c370d8c3486ca85423dd70440c5e0a2aa2 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/72/f3b798bbbb26acba28d5330fe9eebe06debe34: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/72/f3b798bbbb26acba28d5330fe9eebe06debe34 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/77/4ca4a32281246805d4f37ae5e04899c7fd0586: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/77/4ca4a32281246805d4f37ae5e04899c7fd0586 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/80/ba94135cc378364af9d3cb2450df48e51faf2c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/80/ba94135cc378364af9d3cb2450df48e51faf2c -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/a1/fd29ec14823d8bc4a8d1a2cfe35451580f5118: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/a1/fd29ec14823d8bc4a8d1a2cfe35451580f5118 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/a5/196d1be8fb59edf8062bef36d3a602e0812139: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/a5/196d1be8fb59edf8062bef36d3a602e0812139 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/a5/d7b84a673458d14d9aab082183a1968c2c7492: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/a5/d7b84a673458d14d9aab082183a1968c2c7492 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/a9/ab4b760fac7cb80cf8acedd61450a28b995f25: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/a9/ab4b760fac7cb80cf8acedd61450a28b995f25 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/b5/5153268dfe9798446fba79370025190d6e7854: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/b5/5153268dfe9798446fba79370025190d6e7854 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/b5/8d1184a9d43a39c0d95f32453efc78581877d6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/b5/8d1184a9d43a39c0d95f32453efc78581877d6 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/c2/39ea693db9eb39c2a8f54e63f4791c6f1871cc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/c2/39ea693db9eb39c2a8f54e63f4791c6f1871cc -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/c3/8c8524069788e28ebc7d850d0ced46909b4cd0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/c3/8c8524069788e28ebc7d850d0ced46909b4cd0 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/cb/089cd89a7d7686d284d8761201649346b5aa1c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/cb/089cd89a7d7686d284d8761201649346b5aa1c -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/cb/539f07601817f27a4bd1683a645071473a9f85: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/cb/539f07601817f27a4bd1683a645071473a9f85 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/ea/57c1462692c22c42cd8a15a7191808ea8cd947: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/ea/57c1462692c22c42cd8a15a7191808ea8cd947 -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/ec/17ec1939b7c3e86b7cb6c0c4de6b0818a7e75e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/ec/17ec1939b7c3e86b7cb6c0c4de6b0818a7e75e -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/objects/ef/94fa293800b31e3982204f43b13785afebd9bd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kalekundert/vim-coiled-snake/HEAD/tests/issues/dummy_repo_16/git/objects/ef/94fa293800b31e3982204f43b13785afebd9bd -------------------------------------------------------------------------------- /tests/issues/test_21.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | def function(): 4 | print( 5 | "This is a string that should \ 6 | be folded \ 7 | completely" 8 | ) 9 | return True 10 | 11 | function() 12 | -------------------------------------------------------------------------------- /tests/vimrc_explicit_sign_width: -------------------------------------------------------------------------------- 1 | set cpoptions-=C 2 | 3 | call plug#begin() 4 | Plug '~/vim-coiled-snake' 5 | Plug 'Konfekt/FastFold' 6 | call plug#end() 7 | 8 | set signcolumn=yes 9 | let g:coiled_snake_explicit_sign_width=5 10 | 11 | -------------------------------------------------------------------------------- /tests/issues/test_29.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | payload = ( 4 | """\ 5 | { 6 | "ProtocolVersion": 1, 7 | "WebSocketPort": {%s} 8 | }""" 9 | % servers.websocket_server.port 10 | ) 11 | self.send_response(200) 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/corner_cases/complex_parens.py: -------------------------------------------------------------------------------- 1 | _load_groups( 2 | obj, 3 | groups=get_meta(obj).layer_groups[:], 4 | predicate=lambda g: ( 5 | g.is_loaded and 6 | _is_selected_by_cls(g.config, config_cls) 7 | ), 8 | ) 9 | -------------------------------------------------------------------------------- /tests/easy/data_struct.py: -------------------------------------------------------------------------------- 1 | foo = [ 2 | 1, 3 | 2, 4 | 3, 5 | [ 6 | 4, 7 | 5, 8 | 6, 9 | 7, 10 | 8, 11 | 9, 12 | ], 13 | ] 14 | 15 | bar = 1 16 | 17 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /tests/corner_cases/multiline_str_with_paren.py: -------------------------------------------------------------------------------- 1 | # The parentheses should end, allowing the `foo()` function to fold properly, 2 | # even though the multiline string ends on the same line. 3 | 4 | ( 5 | """\ 6 | multline string 7 | """) 8 | 9 | def foo(): 10 | pass 11 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/test_16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # The bar() function should have gitgutter signs and the number of lines in 4 | # each fold should be aligned with the right side of the window. 5 | 6 | def foo(): 7 | pass 8 | 9 | def bar(): 10 | pass 11 | -------------------------------------------------------------------------------- /tests/test_label.py: -------------------------------------------------------------------------------- 1 | class Documented: 2 | """ Docstring """ 3 | 4 | def method_1(): 5 | pass 6 | 7 | @staticmethod 8 | def method_2(): 9 | pass 10 | 11 | @classmethod 12 | def method_3(): 13 | """ Docstring """ 14 | pass 15 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /tests/corner_cases/comment_dedent.py: -------------------------------------------------------------------------------- 1 | def f1(): 2 | # folded 3 | # not folded 4 | 5 | def f2(): 6 | def g1(): 7 | # folded 8 | # not folded 9 | def g2(): 10 | # folded 11 | # not folded 12 | # not folded 13 | 14 | def f3(): 15 | # folded 16 | x = 1 17 | # not folded 18 | -------------------------------------------------------------------------------- /tests/issues/test_7.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | class Foo: 4 | def bar(self): 5 | return self.a 6 | 7 | def bam(self): 8 | return self.b 9 | 10 | class XX: 11 | def x(self): 12 | return 'x' 13 | 14 | def foo(): 15 | pass 16 | 17 | def bar(): 18 | pass 19 | -------------------------------------------------------------------------------- /tests/issues/test_4.py: -------------------------------------------------------------------------------- 1 | def functionWithALotOfArgs( 2 | argument0, 3 | argument1, 4 | argument2, 5 | argument3, 6 | argument4, 7 | argument5, 8 | argument6, 9 | argument7, 10 | argument8, 11 | argument9, 12 | ): 13 | """This should be folded 14 | """ 15 | pass 16 | -------------------------------------------------------------------------------- /ftplugin/python/coiledsnake.vim: -------------------------------------------------------------------------------- 1 | if exists('b:coiled_snake_should_fold') 2 | finish 3 | endif 4 | let b:coiled_snake_should_fold = 1 5 | 6 | if !exists('b:undo_ftplugin') 7 | let b:undo_ftplugin = '' 8 | endif 9 | let b:undo_ftplugin = 'unlet b:coiled_snake_should_fold' . b:undo_ftplugin 10 | 11 | " vim: ts=4 sts=4 sw=4 fdm=marker et sr 12 | -------------------------------------------------------------------------------- /tests/corner_cases/inline_list_addition.py: -------------------------------------------------------------------------------- 1 | # The `] + [` line should not start a data structure fold that extends to the 2 | # end of the file, in turn messing up the foo() and bar() folds. 3 | 4 | x = [ 5 | 1, 6 | 2, 7 | ] + [ 8 | 3, 9 | 4, 10 | ] 11 | 12 | def foo(): 13 | pass 14 | 15 | def bar(): 16 | pass 17 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | @decorator 2 | def function(): 3 | pass 4 | 5 | @decorator( 6 | args) 7 | def function(): 8 | pass 9 | 10 | 11 | @decorator 12 | class Class: 13 | 14 | @decorator 15 | def method(): 16 | pass 17 | 18 | @decorator 19 | def method(): 20 | pass 21 | 22 | 23 | @decorator 24 | def method(): 25 | pass 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/issues/vimrc_7: -------------------------------------------------------------------------------- 1 | call plug#begin() 2 | Plug '~/hacking/projects/vim-coiled-snake' 3 | call plug#end() 4 | 5 | function! g:CoiledSnakeConfigureFold(fold) 6 | let is_class = (a:fold.type == 'class') 7 | let is_method = (a:fold.type == 'function' && get(a:fold.parent, 'type', '') == 'class') 8 | 9 | if is_class || is_method 10 | let a:fold.num_blanks_below = 0 11 | endif 12 | 13 | endfunction 14 | -------------------------------------------------------------------------------- /tests/test_blocks.py: -------------------------------------------------------------------------------- 1 | def bar(): 2 | pass 3 | 4 | def foo(): 5 | """ 6 | Docstring 7 | """ 8 | pass 9 | 10 | code = 0 11 | 12 | class Foo: 13 | def foo(): 14 | pass 15 | 16 | def bar(): 17 | pass 18 | 19 | 20 | def baz(): 21 | pass 22 | 23 | code = 2 24 | 25 | class Bar: 26 | pass 27 | 28 | 29 | class Baz: 30 | pass 31 | 32 | code = 1 33 | -------------------------------------------------------------------------------- /tests/issues/test_8.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """docstring docstring.""" 4 | from __future__ import annotations 5 | 6 | from abc import ABC, abstractmethod 7 | from collections import namedtuple 8 | from logging import getLogger 9 | from typing import ( 10 | Mapping, 11 | NamedTuple, 12 | NoReturn, 13 | Optional, 14 | Sequence, 15 | Text, 16 | Tuple, 17 | Type, 18 | Union, 19 | ) 20 | -------------------------------------------------------------------------------- /tests/test_nested.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | def foo(): 3 | def foo(): 4 | 5 | def foo(): 6 | 7 | 8 | code = 1 9 | def foo(): 10 | pass 11 | code = 1 12 | 13 | code = 1 14 | 15 | class Bar: 16 | class Bar: 17 | class Bar: 18 | 19 | class Bar: 20 | 21 | 22 | code = 1 23 | class Bar: 24 | pass 25 | code = 1 26 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/pre-merge-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git merge" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message to 6 | # stderr if it wants to stop the merge commit. 7 | # 8 | # To enable this hook, rename this file to "pre-merge-commit". 9 | 10 | . git-sh-setup 11 | test -x "$GIT_DIR/hooks/pre-commit" && 12 | exec "$GIT_DIR/hooks/pre-commit" 13 | : 14 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | precommit="$(git rev-parse --git-path hooks/pre-commit)" 13 | test -x "$precommit" && exec "$precommit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /tests/issues/test_12.py: -------------------------------------------------------------------------------- 1 | # Under the following conditions, the return type doesn't get included in the 2 | # fold: 3 | # - Return on a new line with the parenthesis (such as black formats) 4 | # - Whitespace in the type annotation (after comma) 5 | 6 | def get_admission_tables( 7 | profiles: List[str] 8 | ) -> Tuple[pd.DataFrame, pd.DataFrame]: 9 | spam = 1 10 | eggs = 2 11 | 12 | def get_admission_tables( 13 | profiles: List[str] 14 | ) -> Tuple[pd.DataFrame,pd.DataFrame]: 15 | spam = 1 16 | eggs = 2 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | commitmsg="$(git rev-parse --git-path hooks/commit-msg)" 14 | test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /tests/test_everything.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Docstring 3 | ''' 4 | 5 | import foo 6 | import bar 7 | 8 | from bar import baz 9 | from qux import quz 10 | 11 | class Class: 12 | """ Doc """ 13 | 14 | class NestedClass: 15 | pass 16 | 17 | def method(): 18 | def nested_method(): 19 | pass 20 | 21 | @decorator 22 | def decoratee(): 23 | pass 24 | 25 | def function(): 26 | code = 0 27 | more_code = 1 28 | 29 | def nested_function(): 30 | pass 31 | 32 | code = 0 33 | more_code = 1 34 | 35 | if __name__ == '__main__': 36 | pass 37 | -------------------------------------------------------------------------------- /tests/test_docstring.py: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | """ 4 | """ 5 | 6 | """Lorem ipsum 7 | 8 | Dolor sit amet""" 9 | 10 | """ Lorem ipsum 11 | 12 | Dolor sit amet""" 13 | 14 | """ 15 | Lorem ipsum 16 | """ 17 | 18 | """ 19 | Lorem ipsum 20 | 21 | dolor sit amet 22 | """ 23 | 24 | ''' ''' 25 | 26 | ''' 27 | ''' 28 | 29 | '''Lorem ipsum 30 | 31 | Dolor sit amet''' 32 | 33 | ''' Lorem ipsum 34 | 35 | Dolor sit amet''' 36 | 37 | ''' 38 | Lorem ipsum 39 | ''' 40 | 41 | ''' 42 | Lorem ipsum 43 | 44 | dolor sit amet 45 | ''' 46 | 47 | # Don't get confused by mismatched delimiters. 48 | """ 49 | ''' 50 | """ 51 | 52 | ''' 53 | """ 54 | ''' 55 | -------------------------------------------------------------------------------- /tests/corner_cases/comment_after_function.py: -------------------------------------------------------------------------------- 1 | # Related to #22. Adding a lone # after a function should prevent folding as in 2 | # test_ignore.py. Here, including more text after the # (like a comment, or 3 | # code to prevent a linting message), should allow it to fold again. Including 4 | # another # at the end of the line should still prevent folding. 5 | 6 | def foo(): # comment here should still fold 7 | pass 8 | 9 | def foo(): # trailing # still prevents folding # 10 | pass 11 | 12 | @decorator 13 | def foo(): # comment here should still fold 14 | pass 15 | 16 | @decorator 17 | def foo(): # trailing # still prevents folding # 18 | pass 19 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/pre-receive.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to make use of push options. 4 | # The example simply echoes all push options that start with 'echoback=' 5 | # and rejects all pushes when the "reject" push option is used. 6 | # 7 | # To enable this hook, rename this file to "pre-receive". 8 | 9 | if test -n "$GIT_PUSH_OPTION_COUNT" 10 | then 11 | i=0 12 | while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" 13 | do 14 | eval "value=\$GIT_PUSH_OPTION_$i" 15 | case "$value" in 16 | echoback=*) 17 | echo "echo from the pre-receive-hook: ${value#*=}" >&2 18 | ;; 19 | reject) 20 | exit 1 21 | esac 22 | i=$((i + 1)) 23 | done 24 | fi 25 | -------------------------------------------------------------------------------- /tests/corner_cases/quoted_paren.py: -------------------------------------------------------------------------------- 1 | # Each of the `bar()` functions should be folded. 2 | # None of the `a = ...` or `foo(...)` lines should be folded. 3 | 4 | a = "(" 5 | 6 | def bar(): 7 | pass 8 | 9 | a = "\"(" 10 | 11 | def bar(): 12 | pass 13 | 14 | a = "\\\"(" 15 | 16 | def bar(): 17 | pass 18 | 19 | a = '(' 20 | 21 | def bar(): 22 | pass 23 | 24 | a = '\'(' 25 | 26 | def bar(): 27 | pass 28 | 29 | a = '\\\'(' 30 | 31 | def bar(): 32 | pass 33 | 34 | a = '''(''' 35 | 36 | def bar(): 37 | pass 38 | 39 | a = """(""" 40 | 41 | def bar(): 42 | pass 43 | 44 | foo("\\") 45 | 46 | def bar(): 47 | pass 48 | 49 | foo('\\') 50 | 51 | def bar(): 52 | pass 53 | 54 | -------------------------------------------------------------------------------- /tests/vimrc_config_fold: -------------------------------------------------------------------------------- 1 | source vimrc 2 | 3 | function! g:CoiledSnakeConfigureFold(fold) 4 | 5 | " Don't fold nested classes. 6 | if a:fold.type == 'class' 7 | let a:fold.max_level = 1 8 | 9 | " Don't fold nested functions, but do fold methods (i.e. functions 10 | " nested inside classes). 11 | elseif a:fold.type == 'function' 12 | let a:fold.max_level = 1 13 | if get(a:fold.parent, 'type', '') == 'class' 14 | let a:fold.max_level = 2 15 | endif 16 | 17 | " Only fold imports if there are at least 3 of them. 18 | elseif a:fold.type == 'import' 19 | let a:fold.min_lines = 3 20 | endif 21 | 22 | " If the whole program is shorter than 30 lines, don't fold 23 | " anything. 24 | if line('$') < 30 25 | let a:fold.ignore = 1 26 | endif 27 | 28 | endfunction 29 | -------------------------------------------------------------------------------- /tests/test_data_structs.py: -------------------------------------------------------------------------------- 1 | foo = [] 2 | 3 | foo = [ 4 | 1, 5 | ] 6 | 7 | foo = [ 8 | 1] 9 | 10 | foo = [ 11 | 1, 12 | [ 13 | 1, 14 | 2, 15 | 3, 16 | 4, 17 | ], 18 | ] 19 | 20 | foo = () 21 | 22 | foo = ( 23 | 1, 24 | ( 25 | 1, 26 | 2, 27 | 3, 28 | 4, 29 | ), 30 | ) 31 | 32 | foo = ( 33 | 1) 34 | 35 | foo = {} 36 | 37 | foo = { 38 | 1, 39 | { 40 | 1, 41 | 2, 42 | 3, 43 | 4, 44 | }, 45 | } 46 | 47 | foo = { 48 | 1} 49 | 50 | foo = """ """ 51 | 52 | foo = """ 53 | This is 54 | indented 55 | """ 56 | 57 | foo = """ 58 | This is 59 | unindented""" 60 | 61 | foo = ''' ''' 62 | 63 | foo = ''' 64 | This is 65 | indented 66 | ''' 67 | 68 | foo = ''' 69 | This is 70 | unindented''' 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /plugin/coiledsnake.vim: -------------------------------------------------------------------------------- 1 | function! s:setFolds() " {{{1 2 | let w:coiled_snake_folded = 0 3 | 4 | if ! (exists('b:coiled_snake_should_fold') && b:coiled_snake_should_fold) 5 | " not python, reset folds if we had set them earlier 6 | call s:resetFolds() 7 | return 8 | endif 9 | if ! (&foldmethod ==# &g:foldmethod 10 | \&& &foldexpr ==# &g:foldexpr 11 | \&& &foldmethod ==# &g:foldmethod) 12 | " something is not at default value. not safe to fold. 13 | return 14 | endif 15 | 16 | call coiledsnake#LoadSettings() 17 | 18 | if g:coiled_snake_set_foldtext 19 | call coiledsnake#EnableFoldText() 20 | endif 21 | if g:coiled_snake_set_foldexpr 22 | call coiledsnake#EnableFoldExpr() 23 | endif 24 | 25 | let w:coiled_snake_folded = 1 26 | endfunction 27 | 28 | function! s:resetFolds() " {{{1 29 | if (exists('w:coiled_snake_folded') && w:coiled_snake_folded) 30 | call coiledsnake#ResetFoldText() 31 | call coiledsnake#ResetFoldExpr() 32 | endif 33 | endfunction 34 | 35 | augroup CoiledSnake " {{{1 36 | autocmd! 37 | " BufWinEnter to handle Buffers entering windows 38 | " WinNew to handle :split, because it doesn't trigger BufWinEnter 39 | autocmd BufWinEnter,WinNew * call s:setFolds() 40 | augroup END 41 | " }}}1 42 | 43 | " vim: ts=4 sts=4 sw=4 fdm=marker et sr 44 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | while read local_ref local_sha remote_ref remote_sha 28 | do 29 | if [ "$local_sha" = $z40 ] 30 | then 31 | # Handle delete 32 | : 33 | else 34 | if [ "$remote_sha" = $z40 ] 35 | then 36 | # New branch, examine all commits 37 | range="$local_sha" 38 | else 39 | # Update to existing branch, examine new commits 40 | range="$remote_sha..$local_sha" 41 | fi 42 | 43 | # Check for WIP commit 44 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 45 | if [ -n "$commit" ] 46 | then 47 | echo >&2 "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first one removes the 13 | # "# Please enter the commit message..." help message. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | COMMIT_MSG_FILE=$1 24 | COMMIT_SOURCE=$2 25 | SHA1=$3 26 | 27 | /usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" 28 | 29 | # case "$COMMIT_SOURCE,$SHA1" in 30 | # ,|template,) 31 | # /usr/bin/perl -i.bak -pe ' 32 | # print "\n" . `git diff --cached --name-status -r` 33 | # if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; 34 | # *) ;; 35 | # esac 36 | 37 | # SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 38 | # git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" 39 | # if test -z "$COMMIT_SOURCE" 40 | # then 41 | # /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" 42 | # fi 43 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=$(git hash-object -t tree /dev/null) 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | exec git diff-index --check --cached $against -- 50 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | import foo 2 | 3 | code = 1 4 | 5 | import foo 6 | import foo 7 | 8 | code = 1 9 | 10 | import foo 11 | import foo 12 | import foo 13 | 14 | code = 1 15 | 16 | import foo 17 | import foo 18 | import foo 19 | import foo 20 | 21 | code = 1 22 | 23 | import foo 24 | import foo 25 | 26 | # Comments between imports don't break the block. 27 | import foo 28 | import foo 29 | 30 | # Comments before code aren't included in the block. 31 | code = 1 32 | 33 | from foo import foo 34 | 35 | code = 1 36 | 37 | from foo import foo 38 | from foo import foo 39 | 40 | code = 1 41 | 42 | from foo import foo 43 | from foo import foo 44 | from foo import foo 45 | 46 | code = 1 47 | 48 | from foo import foo 49 | from foo import foo 50 | from foo import foo 51 | from foo import foo 52 | 53 | code = 1 54 | 55 | from foo import foo 56 | from foo import foo 57 | 58 | # Comments between imports don't break the block. 59 | from foo import foo 60 | from foo import foo 61 | 62 | # Comments before code aren't included in the block. 63 | code = 1 64 | 65 | # Imports broken across multiple lines should also be folded. 66 | 67 | from foo import () 68 | 69 | code = 1 70 | 71 | from foo import ( 72 | ) 73 | 74 | code = 1 75 | 76 | from foo import ( 77 | foo 78 | ) 79 | 80 | code = 1 81 | 82 | from foo import ( 83 | foo 84 | foo 85 | ) 86 | 87 | code = 1 88 | 89 | from foo import ( 90 | foo 91 | foo 92 | foo 93 | ) 94 | 95 | code = 1 96 | 97 | import foo; foo() 98 | import foo 99 | import foo 100 | import foo 101 | 102 | code = 1 103 | 104 | from foo import \ 105 | bar 106 | 107 | code = 1 108 | 109 | from foo import \ 110 | bar \ 111 | bar 112 | 113 | code = 1 114 | 115 | from foo import \ 116 | bar \ 117 | bar \ 118 | bar 119 | 120 | code = 1 121 | 122 | from foo import \ 123 | bar \ 124 | bar \ 125 | bar \ 126 | bar 127 | 128 | code = 1 129 | 130 | # Mixed imports fold correctly. 131 | 132 | import foo 133 | from foo import foo 134 | from foo import ( 135 | foo 136 | ) 137 | from foo import \ 138 | foo 139 | -------------------------------------------------------------------------------- /tests/issues/vimrc_2: -------------------------------------------------------------------------------- 1 | call plug#begin() 2 | Plug 'tpope/vim-unimpaired' 3 | Plug 'tpope/vim-repeat' 4 | Plug 'tpope/vim-sensible' 5 | Plug 'tpope/vim-sleuth' 6 | Plug 'justinmk/vim-sneak' 7 | Plug 'tpope/vim-dispatch', { 'branch' : 'job' } 8 | " Plug 'yuttie/comfortable-motion.vim' 9 | Plug 'junegunn/vim-peekaboo' 10 | 11 | " Folding and sessions 12 | " Plug 'farmergreg/vim-lastplace' 13 | Plug 'kopischke/vim-stay' 14 | Plug 'kalekundert/vim-coiled-snake' 15 | Plug 'Konfekt/FastFold' 16 | 17 | " Browsing, file management and navigation 18 | Plug 'mhinz/vim-startify' 19 | 20 | if !executable('fzf') 21 | Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --bin' } 22 | endif 23 | Plug 'junegunn/fzf.vim' 24 | Plug 'Alok/notational-fzf-vim' 25 | Plug '~/dev/vim/vim-cards' 26 | Plug 'mhinz/vim-grepper' 27 | 28 | Plug 'francoiscabrol/ranger.vim' 29 | Plug 'christoomey/vim-tmux-navigator' 30 | Plug 'tmux-plugins/vim-tmux-focus-events' 31 | Plug 'roxma/vim-tmux-clipboard' 32 | Plug 'vimwiki/vimwiki' 33 | Plug 'johnsyweb/vim-makeshift' 34 | Plug 'tpope/vim-eunuch' 35 | Plug 'scrooloose/nerdtree' 36 | 37 | Plug 'mkitt/tabline.vim' 38 | Plug 'majutsushi/tagbar' 39 | 40 | Plug 'mtth/scratch.vim' 41 | 42 | " Editing 43 | Plug 'junegunn/goyo.vim' 44 | Plug 'junegunn/limelight.vim' 45 | Plug 'mbbill/undotree' 46 | Plug 'tpope/vim-commentary' 47 | Plug 'godlygeek/tabular' 48 | Plug 'arecarn/vim-crunch' 49 | Plug 'tpope/vim-surround' 50 | Plug 'andymass/vim-matchup' 51 | " Plug 'mg979/vim-yanktools' 52 | Plug 'tpope/vim-endwise' 53 | 54 | " Source control 55 | Plug 'tpope/vim-fugitive' 56 | Plug 'junegunn/gv.vim' 57 | Plug 'tpope/vim-rhubarb' 58 | Plug 'mhinz/vim-signify' 59 | 60 | " Completion 61 | " Plug 'lifepillar/vim-mucomplete' 62 | " Plug 'davidhalter/jedi-vim' 63 | Plug 'SirVer/ultisnips' 64 | Plug 'honza/vim-snippets' 65 | 66 | Plug 'Shougo/deoplete.nvim', { 'do' : ':UpdateRemotePlugins' } 67 | Plug 'roxma/nvim-yarp' 68 | Plug 'roxma/vim-hug-neovim-rpc' 69 | " Plug 'tweekmonster/deoplete-clang2' 70 | " Plug 'Rip-Rip/clang_complete' 71 | Plug 'zchee/deoplete-jedi' 72 | Plug 'zchee/deoplete-clang' 73 | 74 | " Plug 'Shougo/deoplete-clangx' 75 | Plug 'Shougo/neoinclude.vim' 76 | 77 | 78 | " Languages and filetypes 79 | Plug 'vim-syntastic/syntastic' 80 | Plug 'ludovicchabant/vim-gutentags' 81 | Plug 'tpope/vim-apathy' 82 | 83 | Plug 'lervag/vimtex' 84 | Plug 'gauteh/vim-evince-synctex' 85 | Plug 'adborden/vim-notmuch-address' 86 | Plug 'jamessan/vim-gnupg' 87 | " Plug '~/dev/vim/evince-synctex' 88 | 89 | " Color schemes and themeing 90 | Plug 'joshdick/onedark.vim' 91 | Plug 'romainl/Apprentice' 92 | Plug 'rakr/vim-one' 93 | Plug 'endel/vim-github-colorscheme' 94 | Plug 'reedes/vim-colors-pencil' 95 | Plug 'nanotech/jellybeans.vim' 96 | Plug 'roosta/vim-srcery' 97 | Plug 'tyrannicaltoucan/vim-quantum' 98 | 99 | Plug 'vim-airline/vim-airline' 100 | Plug 'vim-airline/vim-airline-themes' 101 | 102 | call plug#end() 103 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/fsmonitor-watchman.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use IPC::Open2; 6 | 7 | # An example hook script to integrate Watchman 8 | # (https://facebook.github.io/watchman/) with git to speed up detecting 9 | # new and modified files. 10 | # 11 | # The hook is passed a version (currently 1) and a time in nanoseconds 12 | # formatted as a string and outputs to stdout all files that have been 13 | # modified since the given time. Paths must be relative to the root of 14 | # the working tree and separated by a single NUL. 15 | # 16 | # To enable this hook, rename this file to "query-watchman" and set 17 | # 'git config core.fsmonitor .git/hooks/query-watchman' 18 | # 19 | my ($version, $time) = @ARGV; 20 | 21 | # Check the hook interface version 22 | 23 | if ($version == 1) { 24 | # convert nanoseconds to seconds 25 | # subtract one second to make sure watchman will return all changes 26 | $time = int ($time / 1000000000) - 1; 27 | } else { 28 | die "Unsupported query-fsmonitor hook version '$version'.\n" . 29 | "Falling back to scanning...\n"; 30 | } 31 | 32 | my $git_work_tree; 33 | if ($^O =~ 'msys' || $^O =~ 'cygwin') { 34 | $git_work_tree = Win32::GetCwd(); 35 | $git_work_tree =~ tr/\\/\//; 36 | } else { 37 | require Cwd; 38 | $git_work_tree = Cwd::cwd(); 39 | } 40 | 41 | my $retry = 1; 42 | 43 | launch_watchman(); 44 | 45 | sub launch_watchman { 46 | 47 | my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') 48 | or die "open2() failed: $!\n" . 49 | "Falling back to scanning...\n"; 50 | 51 | # In the query expression below we're asking for names of files that 52 | # changed since $time but were not transient (ie created after 53 | # $time but no longer exist). 54 | # 55 | # To accomplish this, we're using the "since" generator to use the 56 | # recency index to select candidate nodes and "fields" to limit the 57 | # output to file names only. 58 | 59 | my $query = <<" END"; 60 | ["query", "$git_work_tree", { 61 | "since": $time, 62 | "fields": ["name"] 63 | }] 64 | END 65 | 66 | print CHLD_IN $query; 67 | close CHLD_IN; 68 | my $response = do {local $/; }; 69 | 70 | die "Watchman: command returned no output.\n" . 71 | "Falling back to scanning...\n" if $response eq ""; 72 | die "Watchman: command returned invalid output: $response\n" . 73 | "Falling back to scanning...\n" unless $response =~ /^\{/; 74 | 75 | my $json_pkg; 76 | eval { 77 | require JSON::XS; 78 | $json_pkg = "JSON::XS"; 79 | 1; 80 | } or do { 81 | require JSON::PP; 82 | $json_pkg = "JSON::PP"; 83 | }; 84 | 85 | my $o = $json_pkg->new->utf8->decode($response); 86 | 87 | if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { 88 | print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; 89 | $retry--; 90 | qx/watchman watch "$git_work_tree"/; 91 | die "Failed to make watchman watch '$git_work_tree'.\n" . 92 | "Falling back to scanning...\n" if $? != 0; 93 | 94 | # Watchman will always return all files on the first query so 95 | # return the fast "everything is dirty" flag to git and do the 96 | # Watchman query just to get it over with now so we won't pay 97 | # the cost in git to look up each individual file. 98 | print "/\0"; 99 | eval { launch_watchman() }; 100 | exit 0; 101 | } 102 | 103 | die "Watchman: $o->{error}.\n" . 104 | "Falling back to scanning...\n" if $o->{error}; 105 | 106 | binmode STDOUT, ":utf8"; 107 | local $, = "\0"; 108 | print @{$o->{files}}; 109 | } 110 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to block unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /tests/issues/dummy_repo_16/git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up to date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | <<\DOC_END 92 | 93 | This sample hook safeguards topic branches that have been 94 | published from being rewound. 95 | 96 | The workflow assumed here is: 97 | 98 | * Once a topic branch forks from "master", "master" is never 99 | merged into it again (either directly or indirectly). 100 | 101 | * Once a topic branch is fully cooked and merged into "master", 102 | it is deleted. If you need to build on top of it to correct 103 | earlier mistakes, a new topic branch is created by forking at 104 | the tip of the "master". This is not strictly necessary, but 105 | it makes it easier to keep your history simple. 106 | 107 | * Whenever you need to test or publish your changes to topic 108 | branches, merge them into "next" branch. 109 | 110 | The script, being an example, hardcodes the publish branch name 111 | to be "next", but it is trivial to make it configurable via 112 | $GIT_DIR/config mechanism. 113 | 114 | With this workflow, you would want to know: 115 | 116 | (1) ... if a topic branch has ever been merged to "next". Young 117 | topic branches can have stupid mistakes you would rather 118 | clean up before publishing, and things that have not been 119 | merged into other branches can be easily rebased without 120 | affecting other people. But once it is published, you would 121 | not want to rewind it. 122 | 123 | (2) ... if a topic branch has been fully merged to "master". 124 | Then you can delete it. More importantly, you should not 125 | build on top of it -- other people may already want to 126 | change things related to the topic as patches against your 127 | "master", so if you need further changes, it is better to 128 | fork the topic (perhaps with the same name) afresh from the 129 | tip of "master". 130 | 131 | Let's look at this example: 132 | 133 | o---o---o---o---o---o---o---o---o---o "next" 134 | / / / / 135 | / a---a---b A / / 136 | / / / / 137 | / / c---c---c---c B / 138 | / / / \ / 139 | / / / b---b C \ / 140 | / / / / \ / 141 | ---o---o---o---o---o---o---o---o---o---o---o "master" 142 | 143 | 144 | A, B and C are topic branches. 145 | 146 | * A has one fix since it was merged up to "next". 147 | 148 | * B has finished. It has been fully merged up to "master" and "next", 149 | and is ready to be deleted. 150 | 151 | * C has not merged to "next" at all. 152 | 153 | We would want to allow C to be rebased, refuse A, and encourage 154 | B to be deleted. 155 | 156 | To compute (1): 157 | 158 | git rev-list ^master ^topic next 159 | git rev-list ^master next 160 | 161 | if these match, topic has not merged in next at all. 162 | 163 | To compute (2): 164 | 165 | git rev-list master..topic 166 | 167 | if this is empty, it is fully merged to "master". 168 | 169 | DOC_END 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Coiled Snake: Python Folding for Vim 2 | ==================================== 3 | Coiled Snake is a vim plugin that provides automatic folding of python code. 4 | Its priorities are: (i) to make folds that are satisfyingly compact, (ii) to be 5 | attractive and unobtrusive, (iii) to be robust to any kind of style or 6 | formatting, and (iv) to be highly configurable. Here's what it looks like in 7 | action: 8 | 9 |

10 | 11 |

12 | 13 | A couple features are worth drawing attention to: 14 | 15 | - Classes, functions, docstrings, imports, and large data structures are all 16 | automatically recognized and folded. 17 | - The folds seek to hide as much unnecessary clutter as possible, e.g. 18 | blank lines between methods or classes, docstring quotes, decorator lines, 19 | etc. 20 | - Each fold is labeled clearly and without visual clutter. The labels 21 | can also present context-specific information, like whether a class or 22 | function has been documented. 23 | - The algorithms for automatically creating the folds and summarizing the folds 24 | are independent from each other, so if you only like one you don't have to 25 | use the other. 26 | 27 | Installation 28 | ------------ 29 | Coiled Snake is compatible with both ``vim>=7.4`` and ``neovim``, and can be 30 | installed using any of the plugin management tools out there. I recommend 31 | also installing the [FastFold](https://github.com/Konfekt/FastFold) plugin, 32 | since I find that it makes folding more responsive and less finicky, but it's 33 | not required. 34 | 35 | ### [pathogen](https://github.com/tpope/vim-pathogen) 36 | 37 | Clone this repository into your ``.vim/bundle`` directory: 38 | 39 | cd ~/.vim/bundle 40 | git clone git://github.com/kalekundert/vim-coiled-snake.git 41 | git clone git://github.com/Konfekt/FastFold 42 | 43 | ### [vim-plug](https://github.com/junegunn/vim-plug) 44 | 45 | Put the following line(s) in the ``call plug#begin()`` section of your ``.vimrc`` 46 | file: 47 | 48 | Plug 'kalekundert/vim-coiled-snake' 49 | Plug 'Konfekt/FastFold' 50 | 51 | ### [Vim8 native plugins](https://vimhelp.org/repeat.txt.html#packages) 52 | 53 | Clone the repository into ``.vim/pack/*/start``: 54 | 55 | mkdir -p ~/.vim/pack/git-plugins/start 56 | cd ~/.vim/pack/git-plugins/start 57 | git clone git://github.com/kalekundert/vim-coiled-snake.git 58 | 59 | Note that you can name the directories in ``.vim/pack/`` whatever you like, so 60 | the ``git-plugins`` name in the snippet above is just an example. Also be sure 61 | to enable the following option in your ``.vimrc`` file:: 62 | 63 | filetype plugin indent on 64 | 65 | Usage 66 | ----- 67 | Coiled Snake works with all the standard folding commands. See [``:help 68 | fold-commands``](https://neovim.io/doc/user/fold.html) if you're 69 | not familiar, but below are the commands I use most frequently: 70 | 71 | - ``zo``: Open a fold 72 | - ``zc``: Close a fold 73 | - ``zk``: Jump to the previous fold. 74 | - ``zj``: Jump to the next fold. 75 | - ``zR``: Open every fold. 76 | - ``zM``: Close every fold. 77 | 78 | You can prevent Coiled Snake from folding a line that it otherwise would by 79 | putting a ``#`` at the end of said line. For example, the following function 80 | would not be folded: 81 | 82 | def not_worth_folding(): # 83 | return 42 84 | 85 | Configuration 86 | ------------- 87 | No configuration is necessary, but the following options are available: 88 | 89 | - ``g:coiled_snake_set_foldtext`` (default: ``1``) 90 | 91 | If false, don't load the algorithm for labeling folds. 92 | 93 | - ``g:coiled_snake_set_foldexpr`` (default: ``1``) 94 | 95 | If false, don't load the algorithm for making folds. 96 | 97 | - ``g:coiled_snake_foldtext_flags`` (default: ``['doc', 'static']``) 98 | 99 | A list of the annotations (if any) you want to appear in the fold summaries. 100 | The following values are understood: 101 | - ``'doc'``: Documented classes and functions. 102 | - ``'static'``: Static and class methods. 103 | 104 | - ``g:coiled_snake_explicit_sign_width`` (default: ``0``) 105 | 106 | Explicitly set the width of the sign column used by plugins such as 107 | [vim-gitgutter](https://github.com/airblade/vim-gitgutter). This width is 108 | determined automatically in most cases, but this setting may be useful if the 109 | automatic determination fails. 110 | 111 | - ``g:CoiledSnakeConfigureFold(fold)`` 112 | 113 | This function is called on each automatically-identified fold to customize 114 | how it should behave. The argument is a ``Fold`` object, which describes all 115 | aspects of the fold in question (e.g. where does it start and end, is it 116 | nested in another fold, should it be folded at all, how should trailing blank 117 | lines be handled, etc). By interacting with this object, you can do a lot to 118 | control how the code is folded. 119 | 120 | The best way to illustrate this is with an example: 121 | 122 | function! g:CoiledSnakeConfigureFold(fold) 123 | 124 | " Don't fold nested classes. 125 | if a:fold.type == 'class' 126 | let a:fold.max_level = 1 127 | 128 | " Don't fold nested functions, but do fold methods (i.e. functions 129 | " nested inside a class). 130 | elseif a:fold.type == 'function' 131 | let a:fold.max_level = 1 132 | if get(a:fold.parent, 'type', '') == 'class' 133 | let a:fold.max_level = 2 134 | endif 135 | 136 | " Only fold imports if there are 3 or more of them. 137 | elseif a:fold.type == 'import' 138 | let a:fold.min_lines = 3 139 | endif 140 | 141 | " Don't fold anything if the whole program is shorter than 30 lines. 142 | if line('$') < 30 143 | let a:fold.ignore = 1 144 | endif 145 | 146 | endfunction 147 | 148 | By default, import blocks are only folded if they are 4 lines or longer, 149 | class blocks collapse up to 2 trailing blank lines, function blocks 150 | collapse up to 1 trailing blank line, and data structure blocks (e.g. 151 | literal lists, dicts, sets) are only folded if they are unindented and 152 | longer than 6 lines. 153 | 154 | ``Fold`` object attributes: 155 | 156 | - ``type`` (str, read-only): The kind of lines being folded. The following 157 | values are possible: ``'import'``, ``'decorator'``, ``'class'``, 158 | ``'function'``, ``'struct'``, ``'doc'``. 159 | 160 | - ``parent`` (Fold, read-only): The fold containing this one, or ``{}`` if 161 | this is a top level fold. 162 | 163 | - ``lnum`` (int, read-only): The line number (1-indexed) of the first line in 164 | the fold. 165 | 166 | - ``indent`` (int, read-only): The indent on the first line of the fold. 167 | 168 | - ``level`` (int, read-only): How nested the fold is, where 1 indicates 169 | no nesting. This is based on the indent of the first line in the fold. 170 | 171 | - ``min_lines`` (int): If the fold would include fewer lines than this, it 172 | will not be created. 173 | 174 | - ``max_indent`` (int): If the fold would be more indented than this, it will 175 | not be created. This option is ignored if it's less than 0. 176 | 177 | - ``max_level`` (int): If the fold would have a higher level than this, it 178 | will not be created. This option is ignored if it's less than 0. Note 179 | that this is subtly different than ``max_indent``. For example, consider a 180 | function defined in a for-loop. Because the loop isn't folded, the level 181 | isn't affected while the indent is. 182 | 183 | - ``ignore`` (bool): If true, the fold will not be created. 184 | 185 | - ``num_blanks_below`` (int): The number of trailing blank lines to include 186 | in the fold, if the next fold follows immediately after and is of the same 187 | type and level as this one. This is useful for collapsing the blank space 188 | between classes and methods. 189 | 190 | - ``NumLines()`` (int, read-only): A method that returns the minimum number 191 | of lines that will be included in the fold, not counting any trailing blank 192 | lines that may be collapsed. 193 | 194 | - ``opening_line`` (Line, read-only): A ``Line`` object (see below) 195 | representing the first line of the fold. 196 | 197 | - ``inside_line`` (Line, read-only): A ``Line`` object (see below) 198 | representing the last line that should be included in the fold. The fold 199 | may actually end on a subsequent line, e.g. to collapse trailing blank 200 | lines. 201 | 202 | - ``outside_line`` (Line, read-only): A ``Line`` object (see below) 203 | representing the first line after the fold that should *not* be included in 204 | it. The lines between ``inside_line`` and ``outside_line`` are typically 205 | blank, and may be added to the fold depending on the value of the 206 | ``num_lines_below`` attribute. May be ``{}``, in which case the fold will 207 | end on ``inside_line``. 208 | 209 | ``Line`` object attributes: 210 | 211 | - ``lnum`` (int, read-only): The line number (1-indexed) of the line. 212 | 213 | - ``text`` (str, read-only): The text contained by the line. 214 | 215 | - ``code`` (str, read-only): A modified version of `Line.text` without 216 | strings or comments. 217 | 218 | - ``indent`` (int, read-only): The number of leading spaces on the line. 219 | 220 | - ``ignore_indent`` (bool, read-only): If true, the indentation on this line 221 | is not meaningful to python. This could mean that the line is a comment, 222 | part of a multiline string, wrapped in parentheses, etc. 223 | 224 | - ``paren_level`` (int, read-only): The number of parentheses, brackets, and 225 | braces surrounding this line. 226 | 227 | - ``is_blank`` (bool, read-only): ``1`` if the line contains only whitespace, 228 | ``0`` otherwise. 229 | 230 | - ``is_continuation`` (bool, read-only): If true, this line is part of an 231 | expression that began on a previous line. 232 | 233 | 234 | Troubleshooting 235 | --------------- 236 | - If docstrings don't seems to be folding properly, the problem may be that vim 237 | is running in vi-compatibility mode. Coiled Snake does not work in this 238 | mode, and (for various reasons) docstrings are usually the first symptom. 239 | The solution is to either disable comptibility mode (`:set nocompatible`) or 240 | to specifically allow line continuation in vim scripts (`:set cpoptions-=C`). 241 | 242 | - If anything else seems broken, it may be that the code is crashing in the 243 | middle of making the folds. Vim hides errors that occur during this process, 244 | so you might never know that the code is crashing. To see these errors, run 245 | `:call coiledsnake#DebugFolds()` (if the problem is with the folds 246 | themselves) or `:call coiledsnake#DebugText()` (if the problem is with the 247 | fold labels). If there are any errors, include that information in a [bug 248 | report](https://github.com/kalekundert/vim-coiled-snake/issues). 249 | 250 | Contributing 251 | ------------ 252 | [Bug reports](https://github.com/kalekundert/vim-coiled-snake/issues) and [pull 253 | requests](https://github.com/kalekundert/vim-coiled-snake/pulls) are welcome. I'm especially interested to hear 254 | about cases where the folding algorithm produces goofy results, since it's hard 255 | to encounter every corner case. 256 | -------------------------------------------------------------------------------- /autoload/coiledsnake.vim: -------------------------------------------------------------------------------- 1 | " Patterns {{{1 2 | let s:blank_pattern = '^\s*$' 3 | let s:comment_pattern = '^\s*#' 4 | let s:import_pattern = '^\(import\|from\)' 5 | let s:class_pattern = '^\s*class\s' 6 | let s:function_pattern = '^\s*\(\(async\s\+\)\?def\s\|if __name__\s==\)' 7 | let s:block_pattern = join([s:class_pattern, s:function_pattern], '\|') 8 | let s:decorator_pattern = '^\s*@' 9 | let s:multiline_string_start_pattern = '\v[bBfFrRuU]*(''''''|""")\\?' 10 | let s:multiline_string_start_end_pattern = s:multiline_string_start_pattern . '.*\1' 11 | let s:uniline_string_start_pattern = '\v(''''''|"""|''|")' " triple-quotes first, so that the longest match is made. 12 | let s:uniline_string_end_pattern = '\\@' . l:fold.level 49 | 50 | " If no inside line was found, the fold reaches the end of the file and 51 | " doesn't need to be closed. 52 | if l:fold.inside_line == {} 53 | continue 54 | 55 | " If this fold and the next are the same type and separated only by 56 | " blank lines, allow a certain number of those lines to be included in 57 | " the fold. 58 | elseif l:fold.type == get(l:fold.next_fold, 'type', '') 59 | \ && l:fold.level == get(l:fold.next_fold, 'level') 60 | let closing_lnum = min([ 61 | \ l:fold.inside_line.lnum + l:fold.num_blanks_below, 62 | \ l:fold.outside_line.lnum - 1]) 63 | 64 | " If only an inside line was specified, close the fold exactly on that 65 | " line. 66 | else 67 | let closing_lnum = l:fold.inside_line.lnum 68 | endif 69 | 70 | " Indicate that the fold should be closed, but don't overwrite any 71 | " previous entries. Due to the way the code is organized, any previous 72 | " entries will be higher level folds, and we want those to take 73 | " precedence. 74 | if ! has_key(b:coiled_snake_marks, closing_lnum) 75 | let b:coiled_snake_marks[closing_lnum] = '<' . l:fold.level 76 | endif 77 | 78 | " Ignore folds that end up opening and closing on the same line. 79 | if closing_lnum <= l:fold.lnum 80 | unlet b:coiled_snake_marks[closing_lnum] 81 | endif 82 | endfor 83 | 84 | return b:coiled_snake_marks 85 | endfunction 86 | 87 | function! coiledsnake#FormatText(foldstart, foldend) abort " {{{1 88 | " Find the line that should be used to represent the fold. This is usually 89 | " the first line, but docstrings and decorators are handled specially. 90 | 91 | let focus = {} 92 | let focus.text = getline(a:foldstart) 93 | let focus.offset = 0 94 | 95 | if focus.text =~# s:decorator_pattern 96 | call s:FindDecoratorTitle(focus, a:foldstart, a:foldend) 97 | elseif focus.text =~# s:docstring_pattern 98 | call s:FindDocstringTitle(focus, a:foldstart, a:foldend) 99 | endif 100 | 101 | " Break the line into two parts: a title and a set of flags. The title 102 | " will be left-justified, while the flags will be concatenated together and 103 | " right-justified. 104 | 105 | function! AddBuiltinFlag(flags, flag, condition) 106 | if a:condition && index(g:coiled_snake_foldtext_flags, a:flag) >= 0 107 | call add(a:flags, a:flag) 108 | endif 109 | endfunction 110 | 111 | if focus.text =~# s:block_pattern 112 | let fields = split(focus.text, '#') 113 | let next_line = getline(a:foldstart + focus.offset + 1) 114 | 115 | " Flags are taken to be parenthetical phrases found within an inline 116 | " comment. Line that don't have an inline comment can be trivially 117 | " processed, so this case is handled specially. 118 | 119 | if len(fields) == 1 120 | let title = focus.text 121 | let flags = [] 122 | else 123 | let title = fields[0] 124 | let flags = matchlist(fields[1], '(\([^)]\+\))') 125 | let flags = filter(flags[1:], 'v:val != ""') 126 | endif 127 | 128 | call AddBuiltinFlag(flags, "doc", next_line =~# s:docstring_pattern) 129 | call AddBuiltinFlag(flags, "static", get(focus, 'is_static')) 130 | 131 | else 132 | let title = focus.text 133 | let flags = [] 134 | endif 135 | 136 | " Format a succinct fold message. The title is stripped of whitespace and 137 | " truncated, if it is too long to fit on the screen. The total number of 138 | " folded lines are added as an extra flag, and all the flags are wrapped in 139 | " parenthesis. 140 | 141 | let flags = add(flags, 1 + a:foldend - a:foldstart) 142 | let status = '(' . join(flags, ') (') . ')' 143 | 144 | let cutoff = s:BufferWidth() - strlen(status) 145 | let title = substitute(title, '^\(.\{-}\)\s*$', '\1', '') 146 | 147 | if strlen(title) >= cutoff 148 | let title = title[0:cutoff - 4] . '...' 149 | let padding = '' 150 | else 151 | let padding = cutoff - strlen(title) - 1 152 | let padding = ' ' . repeat(' ', padding) 153 | endif 154 | 155 | if g:coiled_snake_explicit_sign_width != 0 156 | let trailing = g:coiled_snake_explicit_sign_width 157 | let trailing = ' ' . repeat(' ', rightpadding) 158 | else 159 | let trailing = '' 160 | endif 161 | 162 | return title . padding . status . trailing 163 | 164 | endfunction 165 | 166 | function! coiledsnake#LoadSettings() abort "{{{1 167 | call s:SetIfUndef('g:coiled_snake_set_foldexpr', 1) 168 | call s:SetIfUndef('g:coiled_snake_set_foldtext', 1) 169 | call s:SetIfUndef('g:coiled_snake_foldtext_flags', ['doc', 'static']) 170 | call s:SetIfUndef('g:coiled_snake_explicit_sign_width', 0) 171 | endfunction 172 | 173 | function! coiledsnake#EnableFoldText() abort "{{{1 174 | let &l:foldtext = 'coiledsnake#FoldText()' 175 | endfunction 176 | 177 | function! coiledsnake#EnableFoldExpr() abort "{{{1 178 | let &l:foldexpr = 'coiledsnake#FoldExpr(v:lnum)' 179 | let &l:foldmethod = 'expr' 180 | augroup CoiledSnake 181 | autocmd TextChanged,InsertLeave call coiledsnake#ClearFolds() 182 | augroup END 183 | endfunction 184 | 185 | function! coiledsnake#ResetFoldText() abort "{{{1 186 | " Only reset if the value is the same as we initially set 187 | if &foldtext ==# 'coiledsnake#FoldText()' 188 | let &l:foldtext = &g:foldtext 189 | endif 190 | endfunction 191 | 192 | function! coiledsnake#ResetFoldExpr() abort "{{{1 193 | " Only reset if the value is the same as we initially set 194 | if &foldexpr ==# 'coiledsnake#FoldExpr(v:lnum)' 195 | let &l:foldexpr = &g:foldexpr 196 | endif 197 | if &foldmethod ==# 'expr' 198 | let &l:foldmethod = &g:foldmethod 199 | endif 200 | augroup CoiledSnake 201 | autocmd! TextChanged,InsertLeave 202 | augroup END 203 | endfunction 204 | 205 | function! coiledsnake#DebugLines() abort "{{{1 206 | let lines = s:LinesFromBuffer() 207 | 208 | if type(lines) != type([]) 209 | echo "Lines is not a list:" 210 | echo lines 211 | return 212 | endif 213 | 214 | echo "# Blank? Cont? Indent Paren Code" 215 | for lnum in range(0, len(lines)-1) 216 | let line = lines[lnum] 217 | echo printf("%-3s %6s %5s %6s %5s %s", 218 | \ get(line, 'lnum', '???'), 219 | \ get(line, 'is_blank', '?'), 220 | \ get(line, 'is_continuation', '?'), 221 | \ get(line, 'indent', '?'), 222 | \ get(line, 'paren_level', '?'), 223 | \ get(line, 'code', '???')) 224 | endfor 225 | endfunction 226 | 227 | function! coiledsnake#DebugFolds() abort "{{{1 228 | let lines = s:LinesFromBuffer() 229 | let folds = s:FoldsFromLines(lines) 230 | 231 | echo " # In Out Type Parent Lvl Ig N? >? Text" 232 | for lnum in sort(keys(folds), 's:LowToHigh') 233 | let fold = folds[lnum] 234 | echo printf("%3s %3s %3s %-9.9s %-9.9s %3s %2s %2s %2s %s", 235 | \ get(l:fold, 'lnum', '???'), 236 | \ get(l:fold.inside_line, 'lnum', 'EOF'), 237 | \ get(l:fold.outside_line, 'lnum', 'EOF'), 238 | \ get(l:fold, 'type', '???'), 239 | \ get(l:fold.parent, 'type', ''), 240 | \ get(l:fold, 'level', '?'), 241 | \ get(l:fold, 'ignore', '?'), 242 | \ get(l:fold, 'min_lines', '?'), 243 | \ get(l:fold, 'max_level', '?'), 244 | \ get(l:fold.opening_line, 'text', '???')) 245 | endfor 246 | endfunction 247 | 248 | function! coiledsnake#DebugMarks() abort "{{{1 249 | echo "# Fold Line" 250 | for lnum in range(1, line('$')) 251 | echo printf("%-3s %4s %s", 252 | \ lnum, 253 | \ coiledsnake#FoldExpr(lnum), 254 | \ getline(lnum)) 255 | endfor 256 | endfunction 257 | 258 | function! coiledsnake#DebugText() abort "{{{1 259 | echo "# Line" 260 | for lnum in range(1, line('$')) 261 | " This looks better is there's some sort of gutter on the left. 262 | echo lnum coiledsnake#FormatText(lnum, lnum+1) 263 | endfor 264 | endfunction 265 | 266 | function! coiledsnake#DebugBufferWidth() abort "{{{1 267 | echo s:BufferWidth() 268 | endfunction 269 | " }}}1 270 | 271 | function! s:SetIfUndef(name, value) abort " {{{1 272 | if ! exists(a:name) 273 | let {a:name} = a:value 274 | endif 275 | endfunction 276 | 277 | function! s:LinesFromBuffer() abort "{{{1 278 | let lines = [] 279 | let state = {'lines': lines, 'paren_level': 0} 280 | 281 | for lnum in range(1, line('$')) 282 | let line = s:InitLine(lnum, state) 283 | call add(lines, line) 284 | endfor 285 | 286 | return lines 287 | endfunction 288 | 289 | function! s:FoldsFromLines(lines) abort "{{{1 290 | let candidate_folds = {} 291 | let folds = {} 292 | let parents = {} 293 | let prev_fold = {} 294 | let prev_fold_by_level = {} 295 | 296 | " Create a data structure for each possible fold. 297 | for line in a:lines 298 | let l:fold = s:InitFold(line) 299 | if l:fold.type != "" 300 | let candidate_folds[l:fold.lnum] = l:fold 301 | endif 302 | endfor 303 | 304 | " Remove folds that don't meet certain criteria (e.g. number of lines, 305 | " level of indentation, etc.) or have been ignored for some reason (e.g. in 306 | " response to the user, or in the case of decorator and import fold, to 307 | " avoid overlaps). 308 | for lnum in sort(keys(candidate_folds), 's:LowToHigh') 309 | let l:fold = candidate_folds[lnum] 310 | 311 | if l:fold.ignore 312 | continue 313 | endif 314 | 315 | " Figure out (roughly) where to close each fold. This has to be done 316 | " after all the folds have been loaded, so that earlier folds can 317 | " supercede later ones. 318 | call l:fold.FindClosingInfo(a:lines, candidate_folds) 319 | 320 | " Note which lines are included in this fold, so that folds can be 321 | " nested correctly. Initially the nesting was based on indentation, 322 | " but this led to fold levels getting skipped, e.g. if you define a 323 | " function in a for-loop. 324 | let l:fold.parent = get(parents, l:fold.lnum, {}) 325 | let l:fold.level = get(l:fold.parent, 'level', 0) + 1 326 | for lnum in range(l:fold.lnum + 1, l:fold.InsideLnumOrEOF()) 327 | let parents[lnum] = l:fold 328 | endfor 329 | 330 | " Give the user a chance to configure the fold, e.g. set the max size 331 | " or level, decide to ignore it for any reason, etc. 332 | if exists('*g:CoiledSnakeConfigureFold') 333 | call g:CoiledSnakeConfigureFold(l:fold) 334 | endif 335 | 336 | " Duplicate check, in case the `ignore` flag was set by 337 | " `g:CoiledSnakeConfigureFold`. 338 | if l:fold.ignore 339 | continue 340 | endif 341 | 342 | if l:fold.NumLines() < l:fold.min_lines 343 | continue 344 | endif 345 | 346 | if l:fold.max_indent >= 0 && l:fold.opening_line.indent > l:fold.max_indent 347 | continue 348 | endif 349 | 350 | if l:fold.max_level >= 0 && l:fold.level > l:fold.max_level 351 | continue 352 | endif 353 | 354 | let folds[l:fold.lnum] = l:fold 355 | endfor 356 | 357 | " Make note of consecutive folds. Blank lines may be collapsed between 358 | " consecutive folds of the same type. 359 | for lnum in sort(keys(folds), 's:LowToHigh') 360 | let l:fold = folds[lnum] 361 | let l:fold.next_fold = get( 362 | \ folds, 363 | \ get(l:fold.outside_line, 'lnum'), 364 | \ {}) 365 | endfor 366 | 367 | return folds 368 | endfunction 369 | 370 | function! s:InitLine(lnum, state) abort "{{{1 371 | let line = {} 372 | let line.lnum = a:lnum 373 | let line.text = getline(a:lnum) 374 | let line.code = line.text " The line with strings and comments removed. 375 | let line.indent = indent(a:lnum) 376 | let line.ignore_indent = 0 377 | let line.paren_level = 0 378 | let line.is_blank = 0 379 | let line.is_comment = 0 380 | let line.is_continuation = 0 381 | 382 | " Handle strings and comments first, because they will prune non-code 383 | " content that might otherwise confuse later parsers. 384 | call s:InitLineString(line, a:state) 385 | call s:InitLineComment(line, a:state) 386 | call s:InitLineParen(line, a:state) 387 | call s:InitLineBackslash(line, a:state) 388 | call s:InitLineBlank(line, a:state) 389 | 390 | return line 391 | endfunction 392 | 393 | function! s:InitLineString(line, state) "{{{1 394 | " Ignore lines in multiline strings. 395 | if has_key(a:state, 'multiline_string_delim') 396 | let a:line.ignore_indent = 1 397 | 398 | if a:line.code =~# a:state.multiline_string_delim 399 | let a:line.code = 'END' . a:state.multiline_string_delim . 400 | \ split( 401 | \ a:line.code, 402 | \ a:state.multiline_string_delim, 403 | \ 1, 404 | \ )[-1] 405 | unlet a:state.multiline_string_delim 406 | else 407 | let a:line.code = '' 408 | endif 409 | 410 | return 411 | endif 412 | 413 | " Ignore text within strings. 414 | 415 | let a:line.code = substitute( 416 | \ a:line.code, 417 | \ s:uniline_string_pattern, 418 | \ '\1\1', 419 | \ 'g', 420 | \ ) 421 | 422 | " Check to see if this line begins a multiline string. 423 | if a:line.code =~# s:multiline_string_start_pattern && a:line.code !~# s:multiline_string_start_end_pattern 424 | let a:state.multiline_string_delim = matchlist( 425 | \ a:line.code, 426 | \ s:multiline_string_start_pattern 427 | \ )[1] 428 | let a:line.code = split( 429 | \ a:line.code, 430 | \ a:state.multiline_string_delim, 431 | \ 1 432 | \ )[0] 433 | \ . a:state.multiline_string_delim . 'START' 434 | endif 435 | 436 | endfunction 437 | 438 | function! s:InitLineComment(line, state) "{{{1 439 | let a:line.code = substitute(a:line.code, '\s*#.*$', '', '') 440 | if a:line.code =~# s:blank_pattern 441 | let a:line.is_comment = 1 442 | endif 443 | endfunction 444 | 445 | function! s:InitLineParen(line, state) "{{{1 446 | let a:line.paren_level = a:state.paren_level 447 | let a:line.is_continuation = a:line.is_continuation || a:line.paren_level > 0 448 | let a:line.ignore_indent = a:line.ignore_indent || a:line.paren_level > 0 449 | 450 | " Count opening parentheses after the assignment, so that the line with the 451 | " first parenthesis in not considered to be 'parenthesized'. 452 | let a:state.paren_level += count(a:line.code, '(') 453 | let a:state.paren_level += count(a:line.code, '[') 454 | let a:state.paren_level += count(a:line.code, '{') 455 | 456 | let a:state.paren_level -= count(a:line.code, ')') 457 | let a:state.paren_level -= count(a:line.code, ']') 458 | let a:state.paren_level -= count(a:line.code, '}') 459 | endfunction 460 | 461 | function! s:InitLineBackslash(line, state) "{{{1 462 | if has_key(a:state, 'continuation_backslash') 463 | let a:line.ignore_indent = 1 464 | let a:line.is_continuation = 1 465 | unlet a:state.continuation_backslash 466 | endif 467 | 468 | if a:line.code =~# '\\$' 469 | let a:state.continuation_backslash = 1 470 | endif 471 | endfunction 472 | 473 | function! s:InitLineBlank(line, state) "{{{1 474 | " Keep track of blank lines, which can affect where folds end. 475 | 476 | " Use `line.text` instead of `line.code`, because we want to know if the 477 | " line is truly blank. 478 | if a:line.text =~# s:blank_pattern 479 | let a:line.is_blank = 1 480 | endif 481 | endfunction 482 | 483 | function! s:InitFold(line) abort "{{{1 484 | let fold = {} 485 | let fold.parent = {} 486 | let fold.type = "" 487 | let fold.lnum = a:line.lnum 488 | let fold.level = -1 489 | let fold.indent = a:line.indent 490 | let fold.ignore = a:line.text =~# s:manual_ignore_pattern 491 | let fold.min_lines = 0 492 | let fold.max_indent = -1 493 | let fold.max_level = -1 494 | let fold.num_blanks_below = 0 495 | let fold.opening_line = a:line 496 | let fold.inside_line = {} " The last line that should be in the fold. 497 | let fold.outside_line = {} " The first line that shouldn't be in the fold. 498 | let fold.next_fold = {} 499 | let fold.FindClosingInfo = function('s:UndefinedClosingLine') 500 | 501 | function! fold.NumLines() 502 | let l:open = self.opening_line.lnum 503 | let l:close = get(self.inside_line, 'lnum', line('$')) 504 | return l:close - l:open + 1 505 | endfunction 506 | 507 | function! fold.InsideLnumOrEOF() 508 | if self.inside_line == {} 509 | return line('$') 510 | else 511 | return self.inside_line.lnum 512 | endif 513 | endfunction 514 | 515 | if a:line.code =~# s:import_pattern 516 | let fold.type = 'import' 517 | let fold.FindClosingInfo = function('s:CloseImports') 518 | let fold.min_lines = 4 519 | 520 | elseif a:line.code =~# s:decorator_pattern 521 | let fold.type = 'decorator' 522 | let fold.FindClosingInfo = function('s:CloseDecorator') 523 | 524 | elseif a:line.code =~# s:class_pattern 525 | let fold.type = 'class' 526 | let fold.FindClosingInfo = function('s:CloseBlock') 527 | let fold.num_blanks_below = 2 528 | 529 | elseif a:line.code =~# s:function_pattern 530 | let fold.type = 'function' 531 | let fold.FindClosingInfo = function('s:CloseBlock') 532 | let fold.num_blanks_below = 1 533 | 534 | elseif a:line.code =~# s:data_struct_pattern 535 | let fold.type = 'struct' 536 | let fold.FindClosingInfo = function('s:CloseDataStructure') 537 | let fold.max_indent = 0 538 | let fold.min_lines = 6 539 | 540 | elseif a:line.code =~# s:string_struct_pattern 541 | let fold.FindClosingInfo = function('s:CloseOnPattern') 542 | let fold.close_pattern = 'END' . matchlist(a:line.code, s:string_struct_pattern)[1] 543 | 544 | if a:line.code =~# s:docstring_pattern && a:line.paren_level == 0 545 | let fold.type = 'doc' 546 | else 547 | let fold.type = 'struct' 548 | let fold.max_indent = 0 549 | let fold.min_lines = 6 550 | endif 551 | endif 552 | 553 | return fold 554 | endfunction 555 | 556 | function! s:UndefinedClosingLine(lines, folds) abort dict "{{{1 557 | " Identify the last line that should be part of the fold (self.inside_line) 558 | " and (optionally) the first line that should be not be part of it 559 | " (self.outside_line). If this latter information is provided, it may be 560 | " used to include some extra lines in the fold (e.g. blank lines between 561 | " methods). This is a virtual function that must be defined for each type 562 | " of fold (e.g. class, function, docstring, etc.) 563 | throw printf("No algorithm given to end fold of type '%s'", self.type) 564 | endfunction 565 | 566 | function! s:CloseOnPattern(lines, folds) abort dict "{{{1 567 | " `self.lnum` is 1-indexed, indices into `lines` are 0-indexed. 568 | let ii = self.lnum - 1 569 | 570 | for jj in range(ii+1, len(a:lines)-1) 571 | let line = a:lines[jj] 572 | 573 | if has_key(self, 'close_pattern') && line.code =~# self.close_pattern 574 | let self.inside_line = line 575 | return 576 | endif 577 | endfor 578 | endfunction 579 | 580 | function! s:CloseImports(lines, folds) abort dict "{{{1 581 | " `self.lnum` is 1-indexed, indices into `lines` are 0-indexed. 582 | let ii = self.lnum - 1 583 | let self.inside_line = a:lines[ii] 584 | let continuation_paren = 0 585 | let continuation_backslash = 0 586 | 587 | for jj in range(ii, len(a:lines)-1) 588 | let line = a:lines[jj] 589 | 590 | if line.code =~# s:import_pattern || line.is_continuation 591 | let self.inside_line = line 592 | 593 | elseif line.code =~# s:blank_pattern 594 | 595 | else 596 | return 597 | endif 598 | 599 | " Without this, each import line will try to open a new fold. 600 | if jj != ii && has_key(a:folds, line.lnum) 601 | let a:folds[line.lnum].ignore = 1 602 | endif 603 | endfor 604 | endfunction 605 | 606 | function! s:CloseDecorator(lines, folds) abort dict "{{{1 607 | let ii = self.lnum - 1 608 | 609 | " Find the class or function being decorated, and copy it's settings over. 610 | for jj in range(ii+1, len(a:lines)-1) 611 | let line = a:lines[jj] 612 | 613 | if ! has_key(a:folds, line.lnum) 614 | continue 615 | endif 616 | 617 | " Ignore any folds we encounter before the block, but respect if 618 | " they've been manually ignored. 619 | let l:fold = a:folds[line.lnum] 620 | let self.ignore = self.ignore || l:fold.ignore 621 | let l:fold.ignore = 1 622 | 623 | " Copy some settings over from the block we're decorating. 624 | if l:fold.type == 'function' || l:fold.type == 'class' 625 | call l:fold.FindClosingInfo(a:lines, a:folds) 626 | let self.type = l:fold.type 627 | let self.inside_line = l:fold.inside_line 628 | let self.outside_line = l:fold.outside_line 629 | let self.num_blanks_below = l:fold.num_blanks_below 630 | let self.min_lines = l:fold.min_lines + (l:fold.lnum - self.lnum) 631 | return 632 | endif 633 | endfor 634 | endfunction 635 | 636 | function! s:CloseBlock(lines, folds) abort dict "{{{1 637 | " `fold.lnum` is 1-indexed, indices into `lines` are 0-indexed. 638 | let ii = self.lnum - 1 639 | let end = {} 640 | 641 | for jj in range(ii+1, len(a:lines)-1) 642 | let line = a:lines[jj] 643 | let prev_line = a:lines[jj-1] 644 | 645 | " The inside line is the last non-blank line before the change in 646 | " indentation. 647 | if ! prev_line.is_blank 648 | let inside_line = prev_line 649 | endif 650 | 651 | " The outside line is the first line that should not be part of the 652 | " fold, based on indentation. Comments are handled specially. They 653 | " are the outside line if (i) they are dedented and (ii) they aren't 654 | " immediately followed by lines that would be included in the fold. 655 | if line.is_blank || line.ignore_indent 656 | continue 657 | elseif line.indent > self.opening_line.indent 658 | let end = {} 659 | elseif line.is_comment && ! len(end) 660 | let end = {'inside_line': inside_line, 'outside_line': line} 661 | else 662 | if ! len(end) 663 | let end = {'inside_line': inside_line, 'outside_line': line} 664 | endif 665 | break 666 | endif 667 | endfor 668 | 669 | if len(end) 670 | let self.inside_line = end['inside_line'] 671 | let self.outside_line = end['outside_line'] 672 | endif 673 | endfunction 674 | 675 | function! s:CloseDataStructure(lines, folds) abort dict "{{{1 676 | " `self.lnum` is 1-indexed, indices into `lines` are 0-indexed. 677 | let ii = self.lnum - 1 678 | let self.inside_line = a:lines[ii] 679 | 680 | for jj in range(ii+1, len(a:lines)-1) 681 | let line = a:lines[jj] 682 | 683 | if line.paren_level > self.opening_line.paren_level 684 | let self.inside_line = line 685 | else 686 | return 687 | endif 688 | endfor 689 | endfunction 690 | 691 | function! s:FindDecoratorTitle(focus, foldstart, foldend) abort "{{{1 692 | " Step through the fold line-by-line looking for a class or function 693 | " definition. 694 | for offset in range(0, a:foldend - a:foldstart) 695 | let line = getline(a:foldstart + offset) 696 | 697 | " We might want to label static methods. 698 | if line =~# s:decorator_pattern . '\(staticmethod\|classmethod\)' 699 | let a:focus.is_static = 1 700 | endif 701 | 702 | if line =~# s:block_pattern 703 | let a:focus.text = line 704 | let a:focus.offset = offset 705 | return 706 | endif 707 | endfor 708 | endfunction 709 | 710 | function! s:FindDocstringTitle(focus, foldstart, foldend) abort "{{{1 711 | let ii = matchend(a:focus.text, s:multiline_string_start_pattern.'\s*') 712 | let line = strpart(a:focus.text, ii) 713 | let indent = repeat(' ', indent(a:foldstart)) 714 | 715 | if line !~# s:blank_pattern 716 | let a:focus.text = indent . line 717 | return 718 | endif 719 | 720 | for offset in range(1, a:foldend - a:foldstart) 721 | let line = getline(a:foldstart + offset) 722 | 723 | if line !~# s:blank_pattern 724 | let jj = matchend(line, '^\s*') 725 | let a:focus.text = indent . strpart(line, jj) 726 | let a:focus.offset = offset 727 | return 728 | endif 729 | endfor 730 | endfunction 731 | 732 | function! s:LowToHigh(x, y) abort "{{{1 733 | return str2nr(a:x) - str2nr(a:y) 734 | endfunction 735 | 736 | function! s:BufferWidth() abort "{{{1 737 | " Getting the 'usable' window width means dealing with a lot of corner 738 | " cases. See: https://stackoverflow.com/questions/26315925/get-usable-window-width-in-vim-script/52049954#52049954 739 | let width = winwidth(0) 740 | 741 | " If there are line numbers, the `&numberwidth` setting defines their 742 | " minimum width. But we also have to check how many lines are in the file, 743 | " because the actual width will be large enough to accommodate the biggest 744 | " number. 745 | let numberwidth = max([&numberwidth, strlen(line('$')) + 1]) 746 | let numwidth = (&number || &relativenumber) ? numberwidth : 0 747 | 748 | " If present, the column indicating the fold will be one character wide. 749 | let foldwidth = &foldcolumn 750 | 751 | if foldwidth =~# 'auto' "neovim 752 | " we check if the cache exists. if yes, then we check if the cache is 753 | " up-to-date by checking current b:changedtick with that of the cache. 754 | " If they match, we use the cached value for foldwidth 755 | " If they don't match, (ie. buffer changed since last caching), 756 | " we re-calculate foldwidth and save it in the cache 757 | if !exists('b:coiled_snake_cached_foldwidth') || b:changedtick != b:coiled_snake_cached_foldwidth[0] 758 | let maxfoldwidth = (foldwidth =~# 'auto:') ? (split(foldwidth, ':')[1]) : 9 759 | let maxfolddepth = 0 760 | for lnum in range(1, line('$')) 761 | let currentfolddepth = foldlevel(lnum) 762 | if currentfolddepth > maxfolddepth 763 | let maxfolddepth = currentfolddepth 764 | endif 765 | endfor 766 | if maxfolddepth > maxfoldwidth 767 | let foldwidth = maxfoldwidth 768 | else 769 | let foldwidth = maxfolddepth 770 | endif 771 | let b:coiled_snake_cached_foldwidth = deepcopy([b:changedtick, foldwidth]) "deepcopy() due to b:changedtick 772 | else 773 | let foldwidth = b:coiled_snake_cached_foldwidth[1] 774 | endif 775 | endif 776 | 777 | if g:coiled_snake_explicit_sign_width != 0 778 | let signwidth = g:coiled_snake_explicit_sign_width 779 | elseif &signcolumn == 'yes' 780 | let signwidth = 2 781 | elseif &signcolumn =~ 'yes' 782 | let signwidth = &signcolumn 783 | if signwidth =~ ':' 784 | let signwidth = split(signwidth, ':')[1] 785 | endif 786 | let signwidth *= 2 " each signcolumn is 2-char wide 787 | elseif &signcolumn == 'auto' 788 | " The `:sign place` output contains two header lines. 789 | " The sign column is fixed at two columns, if present. 790 | let supports_sign_groups = has('nvim-0.4.2') || has('patch-8.1.614') 791 | let signlist = execute(printf('sign place ' . (supports_sign_groups ? 'group=* ' : '') . 'buffer=%d', bufnr(''))) 792 | let signlist = split(signlist, "\n") 793 | let signwidth = len(signlist) > 2 ? 2 : 0 794 | elseif &signcolumn =~ 'auto' " i.e. neovim 795 | let signwidth = 0 796 | if len(sign_getplaced(bufnr(),{'group':'*'})[0].signs) " signs exist 797 | let signwidth = 0 798 | for l:sign in sign_getplaced(bufnr(),{'group':'*'})[0].signs 799 | let lnum = l:sign.lnum 800 | let signs = len(sign_getplaced(bufnr(),{'group':'*', 'lnum':lnum})[0].signs) 801 | let signwidth = (signs > signwidth ? signs : signwidth) 802 | endfor 803 | endif 804 | let signwidth *= 2 " each signcolumn is 2-char wide 805 | else 806 | let signwidth = 0 807 | endif 808 | 809 | return width - numwidth - foldwidth - signwidth 810 | endfunction 811 | " }}}1 812 | 813 | " vim: ts=4 sts=4 sw=4 fdm=marker et sr 814 | -------------------------------------------------------------------------------- /tests/speed/small_folds.py: -------------------------------------------------------------------------------- 1 | def function(): 2 | pass 3 | 4 | def function(): 5 | pass 6 | 7 | def function(): 8 | pass 9 | 10 | def function(): 11 | pass 12 | 13 | def function(): 14 | pass 15 | 16 | def function(): 17 | pass 18 | 19 | def function(): 20 | pass 21 | 22 | def function(): 23 | pass 24 | 25 | def function(): 26 | pass 27 | 28 | def function(): 29 | pass 30 | 31 | def function(): 32 | pass 33 | 34 | def function(): 35 | pass 36 | 37 | def function(): 38 | pass 39 | 40 | def function(): 41 | pass 42 | 43 | def function(): 44 | pass 45 | 46 | def function(): 47 | pass 48 | 49 | def function(): 50 | pass 51 | 52 | def function(): 53 | pass 54 | 55 | def function(): 56 | pass 57 | 58 | def function(): 59 | pass 60 | 61 | def function(): 62 | pass 63 | 64 | def function(): 65 | pass 66 | 67 | def function(): 68 | pass 69 | 70 | def function(): 71 | pass 72 | 73 | def function(): 74 | pass 75 | 76 | def function(): 77 | pass 78 | 79 | def function(): 80 | pass 81 | 82 | def function(): 83 | pass 84 | 85 | def function(): 86 | pass 87 | 88 | def function(): 89 | pass 90 | 91 | def function(): 92 | pass 93 | 94 | def function(): 95 | pass 96 | 97 | def function(): 98 | pass 99 | 100 | def function(): 101 | pass 102 | 103 | def function(): 104 | pass 105 | 106 | def function(): 107 | pass 108 | 109 | def function(): 110 | pass 111 | 112 | def function(): 113 | pass 114 | 115 | def function(): 116 | pass 117 | 118 | def function(): 119 | pass 120 | 121 | def function(): 122 | pass 123 | 124 | def function(): 125 | pass 126 | 127 | def function(): 128 | pass 129 | 130 | def function(): 131 | pass 132 | 133 | def function(): 134 | pass 135 | 136 | def function(): 137 | pass 138 | 139 | def function(): 140 | pass 141 | 142 | def function(): 143 | pass 144 | 145 | def function(): 146 | pass 147 | 148 | def function(): 149 | pass 150 | 151 | def function(): 152 | pass 153 | 154 | def function(): 155 | pass 156 | 157 | def function(): 158 | pass 159 | 160 | def function(): 161 | pass 162 | 163 | def function(): 164 | pass 165 | 166 | def function(): 167 | pass 168 | 169 | def function(): 170 | pass 171 | 172 | def function(): 173 | pass 174 | 175 | def function(): 176 | pass 177 | 178 | def function(): 179 | pass 180 | 181 | def function(): 182 | pass 183 | 184 | def function(): 185 | pass 186 | 187 | def function(): 188 | pass 189 | 190 | def function(): 191 | pass 192 | 193 | def function(): 194 | pass 195 | 196 | def function(): 197 | pass 198 | 199 | def function(): 200 | pass 201 | 202 | def function(): 203 | pass 204 | 205 | def function(): 206 | pass 207 | 208 | def function(): 209 | pass 210 | 211 | def function(): 212 | pass 213 | 214 | def function(): 215 | pass 216 | 217 | def function(): 218 | pass 219 | 220 | def function(): 221 | pass 222 | 223 | def function(): 224 | pass 225 | 226 | def function(): 227 | pass 228 | 229 | def function(): 230 | pass 231 | 232 | def function(): 233 | pass 234 | 235 | def function(): 236 | pass 237 | 238 | def function(): 239 | pass 240 | 241 | def function(): 242 | pass 243 | 244 | def function(): 245 | pass 246 | 247 | def function(): 248 | pass 249 | 250 | def function(): 251 | pass 252 | 253 | def function(): 254 | pass 255 | 256 | def function(): 257 | pass 258 | 259 | def function(): 260 | pass 261 | 262 | def function(): 263 | pass 264 | 265 | def function(): 266 | pass 267 | 268 | def function(): 269 | pass 270 | 271 | def function(): 272 | pass 273 | 274 | def function(): 275 | pass 276 | 277 | def function(): 278 | pass 279 | 280 | def function(): 281 | pass 282 | 283 | def function(): 284 | pass 285 | 286 | def function(): 287 | pass 288 | 289 | def function(): 290 | pass 291 | 292 | def function(): 293 | pass 294 | 295 | def function(): 296 | pass 297 | 298 | def function(): 299 | pass 300 | 301 | def function(): 302 | pass 303 | 304 | def function(): 305 | pass 306 | 307 | def function(): 308 | pass 309 | 310 | def function(): 311 | pass 312 | 313 | def function(): 314 | pass 315 | 316 | def function(): 317 | pass 318 | 319 | def function(): 320 | pass 321 | 322 | def function(): 323 | pass 324 | 325 | def function(): 326 | pass 327 | 328 | def function(): 329 | pass 330 | 331 | def function(): 332 | pass 333 | 334 | def function(): 335 | pass 336 | 337 | def function(): 338 | pass 339 | 340 | def function(): 341 | pass 342 | 343 | def function(): 344 | pass 345 | 346 | def function(): 347 | pass 348 | 349 | def function(): 350 | pass 351 | 352 | def function(): 353 | pass 354 | 355 | def function(): 356 | pass 357 | 358 | def function(): 359 | pass 360 | 361 | def function(): 362 | pass 363 | 364 | def function(): 365 | pass 366 | 367 | def function(): 368 | pass 369 | 370 | def function(): 371 | pass 372 | 373 | def function(): 374 | pass 375 | 376 | def function(): 377 | pass 378 | 379 | def function(): 380 | pass 381 | 382 | def function(): 383 | pass 384 | 385 | def function(): 386 | pass 387 | 388 | def function(): 389 | pass 390 | 391 | def function(): 392 | pass 393 | 394 | def function(): 395 | pass 396 | 397 | def function(): 398 | pass 399 | 400 | def function(): 401 | pass 402 | 403 | def function(): 404 | pass 405 | 406 | def function(): 407 | pass 408 | 409 | def function(): 410 | pass 411 | 412 | def function(): 413 | pass 414 | 415 | def function(): 416 | pass 417 | 418 | def function(): 419 | pass 420 | 421 | def function(): 422 | pass 423 | 424 | def function(): 425 | pass 426 | 427 | def function(): 428 | pass 429 | 430 | def function(): 431 | pass 432 | 433 | def function(): 434 | pass 435 | 436 | def function(): 437 | pass 438 | 439 | def function(): 440 | pass 441 | 442 | def function(): 443 | pass 444 | 445 | def function(): 446 | pass 447 | 448 | def function(): 449 | pass 450 | 451 | def function(): 452 | pass 453 | 454 | def function(): 455 | pass 456 | 457 | def function(): 458 | pass 459 | 460 | def function(): 461 | pass 462 | 463 | def function(): 464 | pass 465 | 466 | def function(): 467 | pass 468 | 469 | def function(): 470 | pass 471 | 472 | def function(): 473 | pass 474 | 475 | def function(): 476 | pass 477 | 478 | def function(): 479 | pass 480 | 481 | def function(): 482 | pass 483 | 484 | def function(): 485 | pass 486 | 487 | def function(): 488 | pass 489 | 490 | def function(): 491 | pass 492 | 493 | def function(): 494 | pass 495 | 496 | def function(): 497 | pass 498 | 499 | def function(): 500 | pass 501 | 502 | def function(): 503 | pass 504 | 505 | def function(): 506 | pass 507 | 508 | def function(): 509 | pass 510 | 511 | def function(): 512 | pass 513 | 514 | def function(): 515 | pass 516 | 517 | def function(): 518 | pass 519 | 520 | def function(): 521 | pass 522 | 523 | def function(): 524 | pass 525 | 526 | def function(): 527 | pass 528 | 529 | def function(): 530 | pass 531 | 532 | def function(): 533 | pass 534 | 535 | def function(): 536 | pass 537 | 538 | def function(): 539 | pass 540 | 541 | def function(): 542 | pass 543 | 544 | def function(): 545 | pass 546 | 547 | def function(): 548 | pass 549 | 550 | def function(): 551 | pass 552 | 553 | def function(): 554 | pass 555 | 556 | def function(): 557 | pass 558 | 559 | def function(): 560 | pass 561 | 562 | def function(): 563 | pass 564 | 565 | def function(): 566 | pass 567 | 568 | def function(): 569 | pass 570 | 571 | def function(): 572 | pass 573 | 574 | def function(): 575 | pass 576 | 577 | def function(): 578 | pass 579 | 580 | def function(): 581 | pass 582 | 583 | def function(): 584 | pass 585 | 586 | def function(): 587 | pass 588 | 589 | def function(): 590 | pass 591 | 592 | def function(): 593 | pass 594 | 595 | def function(): 596 | pass 597 | 598 | def function(): 599 | pass 600 | 601 | def function(): 602 | pass 603 | 604 | def function(): 605 | pass 606 | 607 | def function(): 608 | pass 609 | 610 | def function(): 611 | pass 612 | 613 | def function(): 614 | pass 615 | 616 | def function(): 617 | pass 618 | 619 | def function(): 620 | pass 621 | 622 | def function(): 623 | pass 624 | 625 | def function(): 626 | pass 627 | 628 | def function(): 629 | pass 630 | 631 | def function(): 632 | pass 633 | 634 | def function(): 635 | pass 636 | 637 | def function(): 638 | pass 639 | 640 | def function(): 641 | pass 642 | 643 | def function(): 644 | pass 645 | 646 | def function(): 647 | pass 648 | 649 | def function(): 650 | pass 651 | 652 | def function(): 653 | pass 654 | 655 | def function(): 656 | pass 657 | 658 | def function(): 659 | pass 660 | 661 | def function(): 662 | pass 663 | 664 | def function(): 665 | pass 666 | 667 | def function(): 668 | pass 669 | 670 | def function(): 671 | pass 672 | 673 | def function(): 674 | pass 675 | 676 | def function(): 677 | pass 678 | 679 | def function(): 680 | pass 681 | 682 | def function(): 683 | pass 684 | 685 | def function(): 686 | pass 687 | 688 | def function(): 689 | pass 690 | 691 | def function(): 692 | pass 693 | 694 | def function(): 695 | pass 696 | 697 | def function(): 698 | pass 699 | 700 | def function(): 701 | pass 702 | 703 | def function(): 704 | pass 705 | 706 | def function(): 707 | pass 708 | 709 | def function(): 710 | pass 711 | 712 | def function(): 713 | pass 714 | 715 | def function(): 716 | pass 717 | 718 | def function(): 719 | pass 720 | 721 | def function(): 722 | pass 723 | 724 | def function(): 725 | pass 726 | 727 | def function(): 728 | pass 729 | 730 | def function(): 731 | pass 732 | 733 | def function(): 734 | pass 735 | 736 | def function(): 737 | pass 738 | 739 | def function(): 740 | pass 741 | 742 | def function(): 743 | pass 744 | 745 | def function(): 746 | pass 747 | 748 | def function(): 749 | pass 750 | 751 | def function(): 752 | pass 753 | 754 | def function(): 755 | pass 756 | 757 | def function(): 758 | pass 759 | 760 | def function(): 761 | pass 762 | 763 | def function(): 764 | pass 765 | 766 | def function(): 767 | pass 768 | 769 | def function(): 770 | pass 771 | 772 | def function(): 773 | pass 774 | 775 | def function(): 776 | pass 777 | 778 | def function(): 779 | pass 780 | 781 | def function(): 782 | pass 783 | 784 | def function(): 785 | pass 786 | 787 | def function(): 788 | pass 789 | 790 | def function(): 791 | pass 792 | 793 | def function(): 794 | pass 795 | 796 | def function(): 797 | pass 798 | 799 | def function(): 800 | pass 801 | 802 | def function(): 803 | pass 804 | 805 | def function(): 806 | pass 807 | 808 | def function(): 809 | pass 810 | 811 | def function(): 812 | pass 813 | 814 | def function(): 815 | pass 816 | 817 | def function(): 818 | pass 819 | 820 | def function(): 821 | pass 822 | 823 | def function(): 824 | pass 825 | 826 | def function(): 827 | pass 828 | 829 | def function(): 830 | pass 831 | 832 | def function(): 833 | pass 834 | 835 | def function(): 836 | pass 837 | 838 | def function(): 839 | pass 840 | 841 | def function(): 842 | pass 843 | 844 | def function(): 845 | pass 846 | 847 | def function(): 848 | pass 849 | 850 | def function(): 851 | pass 852 | 853 | def function(): 854 | pass 855 | 856 | def function(): 857 | pass 858 | 859 | def function(): 860 | pass 861 | 862 | def function(): 863 | pass 864 | 865 | def function(): 866 | pass 867 | 868 | def function(): 869 | pass 870 | 871 | def function(): 872 | pass 873 | 874 | def function(): 875 | pass 876 | 877 | def function(): 878 | pass 879 | 880 | def function(): 881 | pass 882 | 883 | def function(): 884 | pass 885 | 886 | def function(): 887 | pass 888 | 889 | def function(): 890 | pass 891 | 892 | def function(): 893 | pass 894 | 895 | def function(): 896 | pass 897 | 898 | def function(): 899 | pass 900 | 901 | def function(): 902 | pass 903 | 904 | def function(): 905 | pass 906 | 907 | def function(): 908 | pass 909 | 910 | def function(): 911 | pass 912 | 913 | def function(): 914 | pass 915 | 916 | def function(): 917 | pass 918 | 919 | def function(): 920 | pass 921 | 922 | def function(): 923 | pass 924 | 925 | def function(): 926 | pass 927 | 928 | def function(): 929 | pass 930 | 931 | def function(): 932 | pass 933 | 934 | def function(): 935 | pass 936 | 937 | def function(): 938 | pass 939 | 940 | def function(): 941 | pass 942 | 943 | def function(): 944 | pass 945 | 946 | def function(): 947 | pass 948 | 949 | def function(): 950 | pass 951 | 952 | def function(): 953 | pass 954 | 955 | def function(): 956 | pass 957 | 958 | def function(): 959 | pass 960 | 961 | def function(): 962 | pass 963 | 964 | def function(): 965 | pass 966 | 967 | def function(): 968 | pass 969 | 970 | def function(): 971 | pass 972 | 973 | def function(): 974 | pass 975 | 976 | def function(): 977 | pass 978 | 979 | def function(): 980 | pass 981 | 982 | def function(): 983 | pass 984 | 985 | def function(): 986 | pass 987 | 988 | def function(): 989 | pass 990 | 991 | def function(): 992 | pass 993 | 994 | def function(): 995 | pass 996 | 997 | def function(): 998 | pass 999 | 1000 | def function(): 1001 | pass 1002 | 1003 | def function(): 1004 | pass 1005 | 1006 | def function(): 1007 | pass 1008 | 1009 | def function(): 1010 | pass 1011 | 1012 | def function(): 1013 | pass 1014 | 1015 | def function(): 1016 | pass 1017 | 1018 | def function(): 1019 | pass 1020 | 1021 | def function(): 1022 | pass 1023 | 1024 | def function(): 1025 | pass 1026 | 1027 | def function(): 1028 | pass 1029 | 1030 | def function(): 1031 | pass 1032 | 1033 | def function(): 1034 | pass 1035 | 1036 | def function(): 1037 | pass 1038 | 1039 | def function(): 1040 | pass 1041 | 1042 | def function(): 1043 | pass 1044 | 1045 | def function(): 1046 | pass 1047 | 1048 | def function(): 1049 | pass 1050 | 1051 | def function(): 1052 | pass 1053 | 1054 | def function(): 1055 | pass 1056 | 1057 | def function(): 1058 | pass 1059 | 1060 | def function(): 1061 | pass 1062 | 1063 | def function(): 1064 | pass 1065 | 1066 | def function(): 1067 | pass 1068 | 1069 | def function(): 1070 | pass 1071 | 1072 | def function(): 1073 | pass 1074 | 1075 | def function(): 1076 | pass 1077 | 1078 | def function(): 1079 | pass 1080 | 1081 | def function(): 1082 | pass 1083 | 1084 | def function(): 1085 | pass 1086 | 1087 | def function(): 1088 | pass 1089 | 1090 | def function(): 1091 | pass 1092 | 1093 | def function(): 1094 | pass 1095 | 1096 | def function(): 1097 | pass 1098 | 1099 | def function(): 1100 | pass 1101 | 1102 | def function(): 1103 | pass 1104 | 1105 | def function(): 1106 | pass 1107 | 1108 | def function(): 1109 | pass 1110 | 1111 | def function(): 1112 | pass 1113 | 1114 | def function(): 1115 | pass 1116 | 1117 | def function(): 1118 | pass 1119 | 1120 | def function(): 1121 | pass 1122 | 1123 | def function(): 1124 | pass 1125 | 1126 | def function(): 1127 | pass 1128 | 1129 | def function(): 1130 | pass 1131 | 1132 | def function(): 1133 | pass 1134 | 1135 | def function(): 1136 | pass 1137 | 1138 | def function(): 1139 | pass 1140 | 1141 | def function(): 1142 | pass 1143 | 1144 | def function(): 1145 | pass 1146 | 1147 | def function(): 1148 | pass 1149 | 1150 | def function(): 1151 | pass 1152 | 1153 | def function(): 1154 | pass 1155 | 1156 | def function(): 1157 | pass 1158 | 1159 | def function(): 1160 | pass 1161 | 1162 | def function(): 1163 | pass 1164 | 1165 | def function(): 1166 | pass 1167 | 1168 | def function(): 1169 | pass 1170 | 1171 | def function(): 1172 | pass 1173 | 1174 | def function(): 1175 | pass 1176 | 1177 | def function(): 1178 | pass 1179 | 1180 | def function(): 1181 | pass 1182 | 1183 | def function(): 1184 | pass 1185 | 1186 | def function(): 1187 | pass 1188 | 1189 | def function(): 1190 | pass 1191 | 1192 | def function(): 1193 | pass 1194 | 1195 | def function(): 1196 | pass 1197 | 1198 | def function(): 1199 | pass 1200 | 1201 | def function(): 1202 | pass 1203 | 1204 | def function(): 1205 | pass 1206 | 1207 | def function(): 1208 | pass 1209 | 1210 | def function(): 1211 | pass 1212 | 1213 | def function(): 1214 | pass 1215 | 1216 | def function(): 1217 | pass 1218 | 1219 | def function(): 1220 | pass 1221 | 1222 | def function(): 1223 | pass 1224 | 1225 | def function(): 1226 | pass 1227 | 1228 | def function(): 1229 | pass 1230 | 1231 | def function(): 1232 | pass 1233 | 1234 | def function(): 1235 | pass 1236 | 1237 | def function(): 1238 | pass 1239 | 1240 | def function(): 1241 | pass 1242 | 1243 | def function(): 1244 | pass 1245 | 1246 | def function(): 1247 | pass 1248 | 1249 | def function(): 1250 | pass 1251 | 1252 | def function(): 1253 | pass 1254 | 1255 | def function(): 1256 | pass 1257 | 1258 | def function(): 1259 | pass 1260 | 1261 | def function(): 1262 | pass 1263 | 1264 | def function(): 1265 | pass 1266 | 1267 | def function(): 1268 | pass 1269 | 1270 | def function(): 1271 | pass 1272 | 1273 | def function(): 1274 | pass 1275 | 1276 | def function(): 1277 | pass 1278 | 1279 | def function(): 1280 | pass 1281 | 1282 | def function(): 1283 | pass 1284 | 1285 | def function(): 1286 | pass 1287 | 1288 | def function(): 1289 | pass 1290 | 1291 | def function(): 1292 | pass 1293 | 1294 | def function(): 1295 | pass 1296 | 1297 | def function(): 1298 | pass 1299 | 1300 | def function(): 1301 | pass 1302 | 1303 | def function(): 1304 | pass 1305 | 1306 | def function(): 1307 | pass 1308 | 1309 | def function(): 1310 | pass 1311 | 1312 | def function(): 1313 | pass 1314 | 1315 | def function(): 1316 | pass 1317 | 1318 | def function(): 1319 | pass 1320 | 1321 | def function(): 1322 | pass 1323 | 1324 | def function(): 1325 | pass 1326 | 1327 | def function(): 1328 | pass 1329 | 1330 | def function(): 1331 | pass 1332 | 1333 | def function(): 1334 | pass 1335 | 1336 | def function(): 1337 | pass 1338 | 1339 | def function(): 1340 | pass 1341 | 1342 | def function(): 1343 | pass 1344 | 1345 | def function(): 1346 | pass 1347 | 1348 | def function(): 1349 | pass 1350 | 1351 | def function(): 1352 | pass 1353 | 1354 | def function(): 1355 | pass 1356 | 1357 | def function(): 1358 | pass 1359 | 1360 | def function(): 1361 | pass 1362 | 1363 | def function(): 1364 | pass 1365 | 1366 | def function(): 1367 | pass 1368 | 1369 | def function(): 1370 | pass 1371 | 1372 | def function(): 1373 | pass 1374 | 1375 | def function(): 1376 | pass 1377 | 1378 | def function(): 1379 | pass 1380 | 1381 | def function(): 1382 | pass 1383 | 1384 | def function(): 1385 | pass 1386 | 1387 | def function(): 1388 | pass 1389 | 1390 | def function(): 1391 | pass 1392 | 1393 | def function(): 1394 | pass 1395 | 1396 | def function(): 1397 | pass 1398 | 1399 | def function(): 1400 | pass 1401 | 1402 | def function(): 1403 | pass 1404 | 1405 | def function(): 1406 | pass 1407 | 1408 | def function(): 1409 | pass 1410 | 1411 | def function(): 1412 | pass 1413 | 1414 | def function(): 1415 | pass 1416 | 1417 | def function(): 1418 | pass 1419 | 1420 | def function(): 1421 | pass 1422 | 1423 | def function(): 1424 | pass 1425 | 1426 | def function(): 1427 | pass 1428 | 1429 | def function(): 1430 | pass 1431 | 1432 | def function(): 1433 | pass 1434 | 1435 | def function(): 1436 | pass 1437 | 1438 | def function(): 1439 | pass 1440 | 1441 | def function(): 1442 | pass 1443 | 1444 | def function(): 1445 | pass 1446 | 1447 | def function(): 1448 | pass 1449 | 1450 | def function(): 1451 | pass 1452 | 1453 | def function(): 1454 | pass 1455 | 1456 | def function(): 1457 | pass 1458 | 1459 | def function(): 1460 | pass 1461 | 1462 | def function(): 1463 | pass 1464 | 1465 | def function(): 1466 | pass 1467 | 1468 | def function(): 1469 | pass 1470 | 1471 | def function(): 1472 | pass 1473 | 1474 | def function(): 1475 | pass 1476 | 1477 | def function(): 1478 | pass 1479 | 1480 | def function(): 1481 | pass 1482 | 1483 | def function(): 1484 | pass 1485 | 1486 | def function(): 1487 | pass 1488 | 1489 | def function(): 1490 | pass 1491 | 1492 | def function(): 1493 | pass 1494 | 1495 | def function(): 1496 | pass 1497 | 1498 | def function(): 1499 | pass 1500 | 1501 | def function(): 1502 | pass 1503 | 1504 | def function(): 1505 | pass 1506 | 1507 | def function(): 1508 | pass 1509 | 1510 | def function(): 1511 | pass 1512 | 1513 | def function(): 1514 | pass 1515 | 1516 | def function(): 1517 | pass 1518 | 1519 | def function(): 1520 | pass 1521 | 1522 | def function(): 1523 | pass 1524 | 1525 | def function(): 1526 | pass 1527 | 1528 | def function(): 1529 | pass 1530 | 1531 | def function(): 1532 | pass 1533 | 1534 | def function(): 1535 | pass 1536 | 1537 | def function(): 1538 | pass 1539 | 1540 | def function(): 1541 | pass 1542 | 1543 | def function(): 1544 | pass 1545 | 1546 | def function(): 1547 | pass 1548 | 1549 | def function(): 1550 | pass 1551 | 1552 | def function(): 1553 | pass 1554 | 1555 | def function(): 1556 | pass 1557 | 1558 | def function(): 1559 | pass 1560 | 1561 | def function(): 1562 | pass 1563 | 1564 | def function(): 1565 | pass 1566 | 1567 | def function(): 1568 | pass 1569 | 1570 | def function(): 1571 | pass 1572 | 1573 | def function(): 1574 | pass 1575 | 1576 | def function(): 1577 | pass 1578 | 1579 | def function(): 1580 | pass 1581 | 1582 | def function(): 1583 | pass 1584 | 1585 | def function(): 1586 | pass 1587 | 1588 | def function(): 1589 | pass 1590 | 1591 | def function(): 1592 | pass 1593 | 1594 | def function(): 1595 | pass 1596 | 1597 | def function(): 1598 | pass 1599 | 1600 | def function(): 1601 | pass 1602 | 1603 | def function(): 1604 | pass 1605 | 1606 | def function(): 1607 | pass 1608 | 1609 | def function(): 1610 | pass 1611 | 1612 | def function(): 1613 | pass 1614 | 1615 | def function(): 1616 | pass 1617 | 1618 | def function(): 1619 | pass 1620 | 1621 | def function(): 1622 | pass 1623 | 1624 | def function(): 1625 | pass 1626 | 1627 | def function(): 1628 | pass 1629 | 1630 | def function(): 1631 | pass 1632 | 1633 | def function(): 1634 | pass 1635 | 1636 | def function(): 1637 | pass 1638 | 1639 | def function(): 1640 | pass 1641 | 1642 | def function(): 1643 | pass 1644 | 1645 | def function(): 1646 | pass 1647 | 1648 | def function(): 1649 | pass 1650 | 1651 | def function(): 1652 | pass 1653 | 1654 | def function(): 1655 | pass 1656 | 1657 | def function(): 1658 | pass 1659 | 1660 | def function(): 1661 | pass 1662 | 1663 | def function(): 1664 | pass 1665 | 1666 | def function(): 1667 | pass 1668 | 1669 | def function(): 1670 | pass 1671 | 1672 | def function(): 1673 | pass 1674 | 1675 | def function(): 1676 | pass 1677 | 1678 | def function(): 1679 | pass 1680 | 1681 | def function(): 1682 | pass 1683 | 1684 | def function(): 1685 | pass 1686 | 1687 | def function(): 1688 | pass 1689 | 1690 | def function(): 1691 | pass 1692 | 1693 | def function(): 1694 | pass 1695 | 1696 | def function(): 1697 | pass 1698 | 1699 | def function(): 1700 | pass 1701 | 1702 | def function(): 1703 | pass 1704 | 1705 | def function(): 1706 | pass 1707 | 1708 | def function(): 1709 | pass 1710 | 1711 | def function(): 1712 | pass 1713 | 1714 | def function(): 1715 | pass 1716 | 1717 | def function(): 1718 | pass 1719 | 1720 | def function(): 1721 | pass 1722 | 1723 | def function(): 1724 | pass 1725 | 1726 | def function(): 1727 | pass 1728 | 1729 | def function(): 1730 | pass 1731 | 1732 | def function(): 1733 | pass 1734 | 1735 | def function(): 1736 | pass 1737 | 1738 | def function(): 1739 | pass 1740 | 1741 | def function(): 1742 | pass 1743 | 1744 | def function(): 1745 | pass 1746 | 1747 | def function(): 1748 | pass 1749 | 1750 | def function(): 1751 | pass 1752 | 1753 | def function(): 1754 | pass 1755 | 1756 | def function(): 1757 | pass 1758 | 1759 | def function(): 1760 | pass 1761 | 1762 | def function(): 1763 | pass 1764 | 1765 | def function(): 1766 | pass 1767 | 1768 | def function(): 1769 | pass 1770 | 1771 | def function(): 1772 | pass 1773 | 1774 | def function(): 1775 | pass 1776 | 1777 | def function(): 1778 | pass 1779 | 1780 | def function(): 1781 | pass 1782 | 1783 | def function(): 1784 | pass 1785 | 1786 | def function(): 1787 | pass 1788 | 1789 | def function(): 1790 | pass 1791 | 1792 | def function(): 1793 | pass 1794 | 1795 | def function(): 1796 | pass 1797 | 1798 | def function(): 1799 | pass 1800 | 1801 | def function(): 1802 | pass 1803 | 1804 | def function(): 1805 | pass 1806 | 1807 | def function(): 1808 | pass 1809 | 1810 | def function(): 1811 | pass 1812 | 1813 | def function(): 1814 | pass 1815 | 1816 | def function(): 1817 | pass 1818 | 1819 | def function(): 1820 | pass 1821 | 1822 | def function(): 1823 | pass 1824 | 1825 | def function(): 1826 | pass 1827 | 1828 | def function(): 1829 | pass 1830 | 1831 | def function(): 1832 | pass 1833 | 1834 | def function(): 1835 | pass 1836 | 1837 | def function(): 1838 | pass 1839 | 1840 | def function(): 1841 | pass 1842 | 1843 | def function(): 1844 | pass 1845 | 1846 | def function(): 1847 | pass 1848 | 1849 | def function(): 1850 | pass 1851 | 1852 | def function(): 1853 | pass 1854 | 1855 | def function(): 1856 | pass 1857 | 1858 | def function(): 1859 | pass 1860 | 1861 | def function(): 1862 | pass 1863 | 1864 | def function(): 1865 | pass 1866 | 1867 | def function(): 1868 | pass 1869 | 1870 | def function(): 1871 | pass 1872 | 1873 | def function(): 1874 | pass 1875 | 1876 | def function(): 1877 | pass 1878 | 1879 | def function(): 1880 | pass 1881 | 1882 | def function(): 1883 | pass 1884 | 1885 | def function(): 1886 | pass 1887 | 1888 | def function(): 1889 | pass 1890 | 1891 | def function(): 1892 | pass 1893 | 1894 | def function(): 1895 | pass 1896 | 1897 | def function(): 1898 | pass 1899 | 1900 | def function(): 1901 | pass 1902 | 1903 | def function(): 1904 | pass 1905 | 1906 | def function(): 1907 | pass 1908 | 1909 | def function(): 1910 | pass 1911 | 1912 | def function(): 1913 | pass 1914 | 1915 | def function(): 1916 | pass 1917 | 1918 | def function(): 1919 | pass 1920 | 1921 | def function(): 1922 | pass 1923 | 1924 | def function(): 1925 | pass 1926 | 1927 | def function(): 1928 | pass 1929 | 1930 | def function(): 1931 | pass 1932 | 1933 | def function(): 1934 | pass 1935 | 1936 | def function(): 1937 | pass 1938 | 1939 | def function(): 1940 | pass 1941 | 1942 | def function(): 1943 | pass 1944 | 1945 | def function(): 1946 | pass 1947 | 1948 | def function(): 1949 | pass 1950 | 1951 | def function(): 1952 | pass 1953 | 1954 | def function(): 1955 | pass 1956 | 1957 | def function(): 1958 | pass 1959 | 1960 | def function(): 1961 | pass 1962 | 1963 | def function(): 1964 | pass 1965 | 1966 | def function(): 1967 | pass 1968 | 1969 | def function(): 1970 | pass 1971 | 1972 | def function(): 1973 | pass 1974 | 1975 | def function(): 1976 | pass 1977 | 1978 | def function(): 1979 | pass 1980 | 1981 | def function(): 1982 | pass 1983 | 1984 | def function(): 1985 | pass 1986 | 1987 | def function(): 1988 | pass 1989 | 1990 | def function(): 1991 | pass 1992 | 1993 | def function(): 1994 | pass 1995 | 1996 | def function(): 1997 | pass 1998 | 1999 | def function(): 2000 | pass 2001 | 2002 | def function(): 2003 | pass 2004 | 2005 | def function(): 2006 | pass 2007 | 2008 | def function(): 2009 | pass 2010 | 2011 | def function(): 2012 | pass 2013 | 2014 | def function(): 2015 | pass 2016 | 2017 | def function(): 2018 | pass 2019 | 2020 | def function(): 2021 | pass 2022 | 2023 | def function(): 2024 | pass 2025 | 2026 | def function(): 2027 | pass 2028 | 2029 | def function(): 2030 | pass 2031 | 2032 | def function(): 2033 | pass 2034 | 2035 | def function(): 2036 | pass 2037 | 2038 | def function(): 2039 | pass 2040 | 2041 | def function(): 2042 | pass 2043 | 2044 | def function(): 2045 | pass 2046 | 2047 | def function(): 2048 | pass 2049 | 2050 | def function(): 2051 | pass 2052 | 2053 | def function(): 2054 | pass 2055 | 2056 | def function(): 2057 | pass 2058 | 2059 | def function(): 2060 | pass 2061 | 2062 | def function(): 2063 | pass 2064 | 2065 | def function(): 2066 | pass 2067 | 2068 | def function(): 2069 | pass 2070 | 2071 | def function(): 2072 | pass 2073 | 2074 | def function(): 2075 | pass 2076 | 2077 | def function(): 2078 | pass 2079 | 2080 | def function(): 2081 | pass 2082 | 2083 | def function(): 2084 | pass 2085 | 2086 | def function(): 2087 | pass 2088 | 2089 | def function(): 2090 | pass 2091 | 2092 | def function(): 2093 | pass 2094 | 2095 | def function(): 2096 | pass 2097 | 2098 | def function(): 2099 | pass 2100 | 2101 | def function(): 2102 | pass 2103 | 2104 | def function(): 2105 | pass 2106 | 2107 | def function(): 2108 | pass 2109 | 2110 | def function(): 2111 | pass 2112 | 2113 | def function(): 2114 | pass 2115 | 2116 | def function(): 2117 | pass 2118 | 2119 | def function(): 2120 | pass 2121 | 2122 | def function(): 2123 | pass 2124 | 2125 | def function(): 2126 | pass 2127 | 2128 | def function(): 2129 | pass 2130 | 2131 | def function(): 2132 | pass 2133 | 2134 | def function(): 2135 | pass 2136 | 2137 | def function(): 2138 | pass 2139 | 2140 | def function(): 2141 | pass 2142 | 2143 | def function(): 2144 | pass 2145 | 2146 | def function(): 2147 | pass 2148 | 2149 | def function(): 2150 | pass 2151 | 2152 | def function(): 2153 | pass 2154 | 2155 | def function(): 2156 | pass 2157 | 2158 | def function(): 2159 | pass 2160 | 2161 | def function(): 2162 | pass 2163 | 2164 | def function(): 2165 | pass 2166 | 2167 | def function(): 2168 | pass 2169 | 2170 | def function(): 2171 | pass 2172 | 2173 | def function(): 2174 | pass 2175 | 2176 | def function(): 2177 | pass 2178 | 2179 | def function(): 2180 | pass 2181 | 2182 | def function(): 2183 | pass 2184 | 2185 | def function(): 2186 | pass 2187 | 2188 | def function(): 2189 | pass 2190 | 2191 | def function(): 2192 | pass 2193 | 2194 | def function(): 2195 | pass 2196 | 2197 | def function(): 2198 | pass 2199 | 2200 | def function(): 2201 | pass 2202 | 2203 | def function(): 2204 | pass 2205 | 2206 | def function(): 2207 | pass 2208 | 2209 | def function(): 2210 | pass 2211 | 2212 | def function(): 2213 | pass 2214 | 2215 | def function(): 2216 | pass 2217 | 2218 | def function(): 2219 | pass 2220 | 2221 | def function(): 2222 | pass 2223 | 2224 | def function(): 2225 | pass 2226 | 2227 | def function(): 2228 | pass 2229 | 2230 | def function(): 2231 | pass 2232 | 2233 | def function(): 2234 | pass 2235 | 2236 | def function(): 2237 | pass 2238 | 2239 | def function(): 2240 | pass 2241 | 2242 | def function(): 2243 | pass 2244 | 2245 | def function(): 2246 | pass 2247 | 2248 | def function(): 2249 | pass 2250 | 2251 | def function(): 2252 | pass 2253 | 2254 | def function(): 2255 | pass 2256 | 2257 | def function(): 2258 | pass 2259 | 2260 | def function(): 2261 | pass 2262 | 2263 | def function(): 2264 | pass 2265 | 2266 | def function(): 2267 | pass 2268 | 2269 | def function(): 2270 | pass 2271 | 2272 | def function(): 2273 | pass 2274 | 2275 | def function(): 2276 | pass 2277 | 2278 | def function(): 2279 | pass 2280 | 2281 | def function(): 2282 | pass 2283 | 2284 | def function(): 2285 | pass 2286 | 2287 | def function(): 2288 | pass 2289 | 2290 | def function(): 2291 | pass 2292 | 2293 | def function(): 2294 | pass 2295 | 2296 | def function(): 2297 | pass 2298 | 2299 | def function(): 2300 | pass 2301 | 2302 | def function(): 2303 | pass 2304 | 2305 | def function(): 2306 | pass 2307 | 2308 | def function(): 2309 | pass 2310 | 2311 | def function(): 2312 | pass 2313 | 2314 | def function(): 2315 | pass 2316 | 2317 | def function(): 2318 | pass 2319 | 2320 | def function(): 2321 | pass 2322 | 2323 | def function(): 2324 | pass 2325 | 2326 | def function(): 2327 | pass 2328 | 2329 | def function(): 2330 | pass 2331 | 2332 | def function(): 2333 | pass 2334 | 2335 | def function(): 2336 | pass 2337 | 2338 | def function(): 2339 | pass 2340 | 2341 | def function(): 2342 | pass 2343 | 2344 | def function(): 2345 | pass 2346 | 2347 | def function(): 2348 | pass 2349 | 2350 | def function(): 2351 | pass 2352 | 2353 | def function(): 2354 | pass 2355 | 2356 | def function(): 2357 | pass 2358 | 2359 | def function(): 2360 | pass 2361 | 2362 | def function(): 2363 | pass 2364 | 2365 | def function(): 2366 | pass 2367 | 2368 | def function(): 2369 | pass 2370 | 2371 | def function(): 2372 | pass 2373 | 2374 | def function(): 2375 | pass 2376 | 2377 | def function(): 2378 | pass 2379 | 2380 | def function(): 2381 | pass 2382 | 2383 | def function(): 2384 | pass 2385 | 2386 | def function(): 2387 | pass 2388 | 2389 | def function(): 2390 | pass 2391 | 2392 | def function(): 2393 | pass 2394 | 2395 | def function(): 2396 | pass 2397 | 2398 | def function(): 2399 | pass 2400 | 2401 | def function(): 2402 | pass 2403 | 2404 | def function(): 2405 | pass 2406 | 2407 | def function(): 2408 | pass 2409 | 2410 | def function(): 2411 | pass 2412 | 2413 | def function(): 2414 | pass 2415 | 2416 | def function(): 2417 | pass 2418 | 2419 | def function(): 2420 | pass 2421 | 2422 | def function(): 2423 | pass 2424 | 2425 | def function(): 2426 | pass 2427 | 2428 | def function(): 2429 | pass 2430 | 2431 | def function(): 2432 | pass 2433 | 2434 | def function(): 2435 | pass 2436 | 2437 | def function(): 2438 | pass 2439 | 2440 | def function(): 2441 | pass 2442 | 2443 | def function(): 2444 | pass 2445 | 2446 | def function(): 2447 | pass 2448 | 2449 | def function(): 2450 | pass 2451 | 2452 | def function(): 2453 | pass 2454 | 2455 | def function(): 2456 | pass 2457 | 2458 | def function(): 2459 | pass 2460 | 2461 | def function(): 2462 | pass 2463 | 2464 | def function(): 2465 | pass 2466 | 2467 | def function(): 2468 | pass 2469 | 2470 | def function(): 2471 | pass 2472 | 2473 | def function(): 2474 | pass 2475 | 2476 | def function(): 2477 | pass 2478 | 2479 | def function(): 2480 | pass 2481 | 2482 | def function(): 2483 | pass 2484 | 2485 | def function(): 2486 | pass 2487 | 2488 | def function(): 2489 | pass 2490 | 2491 | def function(): 2492 | pass 2493 | 2494 | def function(): 2495 | pass 2496 | 2497 | def function(): 2498 | pass 2499 | 2500 | def function(): 2501 | pass 2502 | 2503 | def function(): 2504 | pass 2505 | 2506 | def function(): 2507 | pass 2508 | 2509 | def function(): 2510 | pass 2511 | 2512 | def function(): 2513 | pass 2514 | 2515 | def function(): 2516 | pass 2517 | 2518 | def function(): 2519 | pass 2520 | 2521 | def function(): 2522 | pass 2523 | 2524 | def function(): 2525 | pass 2526 | 2527 | def function(): 2528 | pass 2529 | 2530 | def function(): 2531 | pass 2532 | 2533 | def function(): 2534 | pass 2535 | 2536 | def function(): 2537 | pass 2538 | 2539 | def function(): 2540 | pass 2541 | 2542 | def function(): 2543 | pass 2544 | 2545 | def function(): 2546 | pass 2547 | 2548 | def function(): 2549 | pass 2550 | 2551 | def function(): 2552 | pass 2553 | 2554 | def function(): 2555 | pass 2556 | 2557 | def function(): 2558 | pass 2559 | 2560 | def function(): 2561 | pass 2562 | 2563 | def function(): 2564 | pass 2565 | 2566 | def function(): 2567 | pass 2568 | 2569 | def function(): 2570 | pass 2571 | 2572 | def function(): 2573 | pass 2574 | 2575 | def function(): 2576 | pass 2577 | 2578 | def function(): 2579 | pass 2580 | 2581 | def function(): 2582 | pass 2583 | 2584 | def function(): 2585 | pass 2586 | 2587 | def function(): 2588 | pass 2589 | 2590 | def function(): 2591 | pass 2592 | 2593 | def function(): 2594 | pass 2595 | 2596 | def function(): 2597 | pass 2598 | 2599 | def function(): 2600 | pass 2601 | 2602 | def function(): 2603 | pass 2604 | 2605 | def function(): 2606 | pass 2607 | 2608 | def function(): 2609 | pass 2610 | 2611 | def function(): 2612 | pass 2613 | 2614 | def function(): 2615 | pass 2616 | 2617 | def function(): 2618 | pass 2619 | 2620 | def function(): 2621 | pass 2622 | 2623 | def function(): 2624 | pass 2625 | 2626 | def function(): 2627 | pass 2628 | 2629 | def function(): 2630 | pass 2631 | 2632 | def function(): 2633 | pass 2634 | 2635 | def function(): 2636 | pass 2637 | 2638 | def function(): 2639 | pass 2640 | 2641 | def function(): 2642 | pass 2643 | 2644 | def function(): 2645 | pass 2646 | 2647 | def function(): 2648 | pass 2649 | 2650 | def function(): 2651 | pass 2652 | 2653 | def function(): 2654 | pass 2655 | 2656 | def function(): 2657 | pass 2658 | 2659 | def function(): 2660 | pass 2661 | 2662 | def function(): 2663 | pass 2664 | 2665 | def function(): 2666 | pass 2667 | 2668 | def function(): 2669 | pass 2670 | 2671 | def function(): 2672 | pass 2673 | 2674 | def function(): 2675 | pass 2676 | 2677 | def function(): 2678 | pass 2679 | 2680 | def function(): 2681 | pass 2682 | 2683 | def function(): 2684 | pass 2685 | 2686 | def function(): 2687 | pass 2688 | 2689 | def function(): 2690 | pass 2691 | 2692 | def function(): 2693 | pass 2694 | 2695 | def function(): 2696 | pass 2697 | 2698 | def function(): 2699 | pass 2700 | 2701 | def function(): 2702 | pass 2703 | 2704 | def function(): 2705 | pass 2706 | 2707 | def function(): 2708 | pass 2709 | 2710 | def function(): 2711 | pass 2712 | 2713 | def function(): 2714 | pass 2715 | 2716 | def function(): 2717 | pass 2718 | 2719 | def function(): 2720 | pass 2721 | 2722 | def function(): 2723 | pass 2724 | 2725 | def function(): 2726 | pass 2727 | 2728 | def function(): 2729 | pass 2730 | 2731 | def function(): 2732 | pass 2733 | 2734 | def function(): 2735 | pass 2736 | 2737 | def function(): 2738 | pass 2739 | 2740 | def function(): 2741 | pass 2742 | 2743 | def function(): 2744 | pass 2745 | 2746 | def function(): 2747 | pass 2748 | 2749 | def function(): 2750 | pass 2751 | 2752 | def function(): 2753 | pass 2754 | 2755 | def function(): 2756 | pass 2757 | 2758 | def function(): 2759 | pass 2760 | 2761 | def function(): 2762 | pass 2763 | 2764 | def function(): 2765 | pass 2766 | 2767 | def function(): 2768 | pass 2769 | 2770 | def function(): 2771 | pass 2772 | 2773 | def function(): 2774 | pass 2775 | 2776 | def function(): 2777 | pass 2778 | 2779 | def function(): 2780 | pass 2781 | 2782 | def function(): 2783 | pass 2784 | 2785 | def function(): 2786 | pass 2787 | 2788 | def function(): 2789 | pass 2790 | 2791 | def function(): 2792 | pass 2793 | 2794 | def function(): 2795 | pass 2796 | 2797 | def function(): 2798 | pass 2799 | 2800 | def function(): 2801 | pass 2802 | 2803 | def function(): 2804 | pass 2805 | 2806 | def function(): 2807 | pass 2808 | 2809 | def function(): 2810 | pass 2811 | 2812 | def function(): 2813 | pass 2814 | 2815 | def function(): 2816 | pass 2817 | 2818 | def function(): 2819 | pass 2820 | 2821 | def function(): 2822 | pass 2823 | 2824 | def function(): 2825 | pass 2826 | 2827 | def function(): 2828 | pass 2829 | 2830 | def function(): 2831 | pass 2832 | 2833 | def function(): 2834 | pass 2835 | 2836 | def function(): 2837 | pass 2838 | 2839 | def function(): 2840 | pass 2841 | 2842 | def function(): 2843 | pass 2844 | 2845 | def function(): 2846 | pass 2847 | 2848 | def function(): 2849 | pass 2850 | 2851 | def function(): 2852 | pass 2853 | 2854 | def function(): 2855 | pass 2856 | 2857 | def function(): 2858 | pass 2859 | 2860 | def function(): 2861 | pass 2862 | 2863 | def function(): 2864 | pass 2865 | 2866 | def function(): 2867 | pass 2868 | 2869 | def function(): 2870 | pass 2871 | 2872 | def function(): 2873 | pass 2874 | 2875 | def function(): 2876 | pass 2877 | 2878 | def function(): 2879 | pass 2880 | 2881 | def function(): 2882 | pass 2883 | 2884 | def function(): 2885 | pass 2886 | 2887 | def function(): 2888 | pass 2889 | 2890 | def function(): 2891 | pass 2892 | 2893 | def function(): 2894 | pass 2895 | 2896 | def function(): 2897 | pass 2898 | 2899 | def function(): 2900 | pass 2901 | 2902 | def function(): 2903 | pass 2904 | 2905 | def function(): 2906 | pass 2907 | 2908 | def function(): 2909 | pass 2910 | 2911 | def function(): 2912 | pass 2913 | 2914 | def function(): 2915 | pass 2916 | 2917 | def function(): 2918 | pass 2919 | 2920 | def function(): 2921 | pass 2922 | 2923 | def function(): 2924 | pass 2925 | 2926 | def function(): 2927 | pass 2928 | 2929 | def function(): 2930 | pass 2931 | 2932 | def function(): 2933 | pass 2934 | 2935 | def function(): 2936 | pass 2937 | 2938 | def function(): 2939 | pass 2940 | 2941 | def function(): 2942 | pass 2943 | 2944 | def function(): 2945 | pass 2946 | 2947 | def function(): 2948 | pass 2949 | 2950 | def function(): 2951 | pass 2952 | 2953 | def function(): 2954 | pass 2955 | 2956 | def function(): 2957 | pass 2958 | 2959 | def function(): 2960 | pass 2961 | 2962 | def function(): 2963 | pass 2964 | 2965 | def function(): 2966 | pass 2967 | 2968 | def function(): 2969 | pass 2970 | 2971 | def function(): 2972 | pass 2973 | 2974 | def function(): 2975 | pass 2976 | 2977 | def function(): 2978 | pass 2979 | 2980 | def function(): 2981 | pass 2982 | 2983 | def function(): 2984 | pass 2985 | 2986 | def function(): 2987 | pass 2988 | 2989 | def function(): 2990 | pass 2991 | 2992 | def function(): 2993 | pass 2994 | 2995 | def function(): 2996 | pass 2997 | 2998 | def function(): 2999 | pass 3000 | 3001 | -------------------------------------------------------------------------------- /tests/issues/test_3.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import datetime 4 | import itertools 5 | import logging 6 | import re 7 | import time 8 | from hashlib import sha1 9 | 10 | import newrelic.agent 11 | from django.conf import settings 12 | from django.contrib.auth.models import User 13 | from django.core.exceptions import ObjectDoesNotExist 14 | from django.core.validators import MinLengthValidator 15 | from django.db import (models, 16 | transaction) 17 | from django.db.models import (Count, 18 | Q) 19 | from django.db.utils import ProgrammingError 20 | from django.forms import model_to_dict 21 | from django.utils import timezone 22 | from django.utils.encoding import python_2_unicode_compatible 23 | 24 | from ..services.elasticsearch import (bulk, 25 | index) 26 | from ..utils.queryset import chunked_qs 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | # MySQL Full Text Search operators, based on: 31 | # https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html 32 | mysql_fts_operators_re = re.compile(r'[-+@<>()~*"]') 33 | 34 | 35 | @python_2_unicode_compatible 36 | class NamedModel(models.Model): 37 | id = models.AutoField(primary_key=True) 38 | name = models.CharField(max_length=100, unique=True) 39 | 40 | class Meta: 41 | abstract = True 42 | 43 | def __str__(self): 44 | return self.name 45 | 46 | 47 | class Product(NamedModel): 48 | class Meta: 49 | db_table = 'product' 50 | 51 | 52 | @python_2_unicode_compatible 53 | class BuildPlatform(models.Model): 54 | id = models.AutoField(primary_key=True) 55 | os_name = models.CharField(max_length=25) 56 | platform = models.CharField(max_length=100, db_index=True) 57 | architecture = models.CharField(max_length=25, blank=True, db_index=True) 58 | 59 | class Meta: 60 | db_table = 'build_platform' 61 | unique_together = ("os_name", "platform", "architecture") 62 | 63 | def __str__(self): 64 | return "{0} {1} {2}".format( 65 | self.os_name, self.platform, self.architecture) 66 | 67 | 68 | class Option(NamedModel): 69 | 70 | class Meta: 71 | db_table = 'option' 72 | 73 | 74 | class RepositoryGroup(NamedModel): 75 | 76 | description = models.TextField(blank=True) 77 | 78 | class Meta: 79 | db_table = 'repository_group' 80 | 81 | 82 | @python_2_unicode_compatible 83 | class Repository(models.Model): 84 | id = models.AutoField(primary_key=True) 85 | repository_group = models.ForeignKey('RepositoryGroup', on_delete=models.CASCADE) 86 | name = models.CharField(max_length=50, unique=True, db_index=True) 87 | dvcs_type = models.CharField(max_length=25, db_index=True) 88 | url = models.CharField(max_length=255) 89 | branch = models.CharField(max_length=50, null=True, db_index=True) 90 | codebase = models.CharField(max_length=50, blank=True, db_index=True) 91 | description = models.TextField(blank=True) 92 | active_status = models.CharField(max_length=7, blank=True, default='active', db_index=True) 93 | performance_alerts_enabled = models.BooleanField(default=False) 94 | expire_performance_data = models.BooleanField(default=True) 95 | is_try_repo = models.BooleanField(default=False) 96 | 97 | class Meta: 98 | db_table = 'repository' 99 | verbose_name_plural = 'repositories' 100 | 101 | def __str__(self): 102 | return "{0} {1}".format( 103 | self.name, self.repository_group) 104 | 105 | 106 | @python_2_unicode_compatible 107 | class Push(models.Model): 108 | ''' 109 | A push to a repository 110 | 111 | A push should contain one or more commit objects, representing 112 | the changesets that were part of the push 113 | ''' 114 | repository = models.ForeignKey(Repository, on_delete=models.CASCADE) 115 | revision = models.CharField(max_length=40) 116 | author = models.CharField(max_length=150) 117 | time = models.DateTimeField(db_index=True) 118 | 119 | class Meta: 120 | db_table = 'push' 121 | unique_together = ('repository', 'revision') 122 | 123 | def __str__(self): 124 | return "{0} {1}".format( 125 | self.repository.name, self.revision) 126 | 127 | def get_status(self): 128 | ''' 129 | Gets a summary of what passed/failed for the push 130 | ''' 131 | jobs = Job.objects.filter(push=self).filter( 132 | Q(failure_classification__isnull=True) | 133 | Q(failure_classification__name='not classified')).exclude(tier=3) 134 | 135 | status_dict = {} 136 | for (state, result, total) in jobs.values_list( 137 | 'state', 'result').annotate( 138 | total=Count('result')): 139 | if state == 'completed': 140 | status_dict[result] = total 141 | else: 142 | status_dict[state] = total 143 | if 'superseded' in status_dict: 144 | # backward compatability for API consumers 145 | status_dict['coalesced'] = status_dict['superseded'] 146 | 147 | return status_dict 148 | 149 | 150 | @python_2_unicode_compatible 151 | class Commit(models.Model): 152 | ''' 153 | A single commit in a push 154 | ''' 155 | push = models.ForeignKey(Push, on_delete=models.CASCADE, related_name='commits') 156 | revision = models.CharField(max_length=40) 157 | author = models.CharField(max_length=150) 158 | comments = models.TextField() 159 | 160 | class Meta: 161 | db_table = 'commit' 162 | unique_together = ('push', 'revision') 163 | 164 | def __str__(self): 165 | return "{0} {1}".format( 166 | self.push.repository.name, self.revision) 167 | 168 | 169 | @python_2_unicode_compatible 170 | class MachinePlatform(models.Model): 171 | id = models.AutoField(primary_key=True) 172 | os_name = models.CharField(max_length=25) 173 | platform = models.CharField(max_length=100, db_index=True) 174 | architecture = models.CharField(max_length=25, blank=True, db_index=True) 175 | 176 | class Meta: 177 | db_table = 'machine_platform' 178 | unique_together = ("os_name", "platform", "architecture") 179 | 180 | def __str__(self): 181 | return "{0} {1} {2}".format( 182 | self.os_name, self.platform, self.architecture) 183 | 184 | 185 | @python_2_unicode_compatible 186 | class Bugscache(models.Model): 187 | id = models.PositiveIntegerField(primary_key=True) 188 | status = models.CharField(max_length=64, db_index=True) 189 | resolution = models.CharField(max_length=64, blank=True, db_index=True) 190 | # Is covered by a FULLTEXT index created via a migrations RunSQL operation. 191 | summary = models.CharField(max_length=255) 192 | crash_signature = models.TextField(blank=True) 193 | keywords = models.TextField(blank=True) 194 | os = models.CharField(max_length=64, blank=True) 195 | modified = models.DateTimeField() 196 | whiteboard = models.CharField(max_length=100, blank=True, default='') 197 | 198 | class Meta: 199 | db_table = 'bugscache' 200 | verbose_name_plural = 'bugscache' 201 | 202 | def __str__(self): 203 | return "{0}".format(self.id) 204 | 205 | @classmethod 206 | def search(cls, search_term): 207 | max_size = 50 208 | 209 | # 90 days ago 210 | time_limit = datetime.datetime.now() - datetime.timedelta(days=90) 211 | 212 | # Replace MySQL's Full Text Search Operators with spaces so searching 213 | # for errors that have been pasted in still works. 214 | sanitised_term = re.sub(mysql_fts_operators_re, " ", search_term) 215 | 216 | # Wrap search term so it is used as a phrase in the full-text search. 217 | search_term_fulltext = '"%s"' % sanitised_term 218 | 219 | # Substitute escape and wildcard characters, so the search term is used 220 | # literally in the LIKE statement. 221 | search_term_like = search_term.replace('=', '==').replace('%', '=%').replace('_', '=_') 222 | 223 | recent_qs = cls.objects.raw( 224 | """ 225 | SELECT id, summary, crash_signature, keywords, os, resolution, status, 226 | MATCH (`summary`) AGAINST (%s IN BOOLEAN MODE) AS relevance 227 | FROM bugscache 228 | WHERE 1 229 | AND resolution = '' 230 | AND `summary` LIKE CONCAT ('%%%%', %s, '%%%%') ESCAPE '=' 231 | AND modified >= %s 232 | ORDER BY relevance DESC 233 | LIMIT 0,%s 234 | """, 235 | [search_term_fulltext, search_term_like, time_limit, max_size], 236 | ) 237 | 238 | try: 239 | open_recent = [model_to_dict(item, exclude=["modified"]) for item in recent_qs] 240 | except ProgrammingError as e: 241 | newrelic.agent.record_exception() 242 | logger.error('Failed to execute FULLTEXT search on Bugscache, error={}, SQL={}'.format(e, recent_qs.query.__str__())) 243 | open_recent = [] 244 | 245 | all_others_qs = cls.objects.raw( 246 | """ 247 | SELECT id, summary, crash_signature, keywords, os, resolution, status, 248 | MATCH (`summary`) AGAINST (%s IN BOOLEAN MODE) AS relevance 249 | FROM bugscache 250 | WHERE 1 251 | AND `summary` LIKE CONCAT ('%%%%', %s, '%%%%') ESCAPE '=' 252 | AND (modified < %s OR resolution <> '') 253 | ORDER BY relevance DESC 254 | LIMIT 0,%s 255 | """, 256 | [search_term_fulltext, search_term_like, time_limit, max_size], 257 | ) 258 | 259 | try: 260 | all_others = [model_to_dict(item, exclude=["modified"]) for item in all_others_qs] 261 | except ProgrammingError as e: 262 | newrelic.agent.record_exception() 263 | logger.error('Failed to execute FULLTEXT search on Bugscache, error={}, SQL={}'.format(e, recent_qs.query.__str__())) 264 | all_others = [] 265 | 266 | return {"open_recent": open_recent, "all_others": all_others} 267 | 268 | 269 | class Machine(NamedModel): 270 | 271 | class Meta: 272 | db_table = 'machine' 273 | 274 | 275 | @python_2_unicode_compatible 276 | class JobGroup(models.Model): 277 | id = models.AutoField(primary_key=True) 278 | symbol = models.CharField(max_length=25, default='?', db_index=True) 279 | name = models.CharField(max_length=100) 280 | description = models.TextField(blank=True) 281 | 282 | class Meta: 283 | db_table = 'job_group' 284 | unique_together = ('name', 'symbol') 285 | 286 | def __str__(self): 287 | return "{0} ({1})".format( 288 | self.name, self.symbol) 289 | 290 | 291 | class OptionCollectionManager(models.Manager): 292 | ''' 293 | Convenience function to determine the option collection map 294 | ''' 295 | def get_option_collection_map(self, options_as_list=False): 296 | option_collection_map = {} 297 | for (hash, option_name) in OptionCollection.objects.values_list( 298 | 'option_collection_hash', 'option__name'): 299 | if options_as_list: 300 | if not option_collection_map.get(hash): 301 | option_collection_map[hash] = [option_name] 302 | else: 303 | option_collection_map[hash].append(option_name) 304 | else: 305 | if not option_collection_map.get(hash): 306 | option_collection_map[hash] = option_name 307 | else: 308 | option_collection_map[hash] += (' ' + option_name) 309 | 310 | return option_collection_map 311 | 312 | 313 | @python_2_unicode_compatible 314 | class OptionCollection(models.Model): 315 | id = models.AutoField(primary_key=True) 316 | option_collection_hash = models.CharField(max_length=40) 317 | option = models.ForeignKey(Option, on_delete=models.CASCADE, db_index=True) 318 | 319 | objects = OptionCollectionManager() 320 | 321 | @staticmethod 322 | def calculate_hash(options): 323 | """returns an option_collection_hash given a list of options""" 324 | options = sorted(list(options)) 325 | sha_hash = sha1() 326 | # equivalent to loop over the options and call sha_hash.update() 327 | sha_hash.update(''.join(options)) 328 | return sha_hash.hexdigest() 329 | 330 | class Meta: 331 | db_table = 'option_collection' 332 | unique_together = ('option_collection_hash', 'option') 333 | 334 | def __str__(self): 335 | return "{0}".format(self.option) 336 | 337 | 338 | @python_2_unicode_compatible 339 | class JobType(models.Model): 340 | id = models.AutoField(primary_key=True) 341 | symbol = models.CharField(max_length=25, default='?', db_index=True) 342 | name = models.CharField(max_length=100) 343 | description = models.TextField(blank=True) 344 | 345 | class Meta: 346 | db_table = 'job_type' 347 | unique_together = (('name', 'symbol'),) 348 | 349 | def __str__(self): 350 | return "{0} ({1})".format( 351 | self.name, self.symbol) 352 | 353 | 354 | class FailureClassification(NamedModel): 355 | 356 | class Meta: 357 | db_table = 'failure_classification' 358 | 359 | 360 | class ReferenceDataSignatures(models.Model): 361 | 362 | """ 363 | A collection of all the possible combinations of reference data, 364 | populated on data ingestion. signature is a hash of the data it refers to 365 | build_system_type is buildbot by default 366 | 367 | TODO: Rename to 'ReferenceDataSignature'. 368 | """ 369 | name = models.CharField(max_length=255) 370 | signature = models.CharField(max_length=50, db_index=True) 371 | build_os_name = models.CharField(max_length=25, db_index=True) 372 | build_platform = models.CharField(max_length=100, db_index=True) 373 | build_architecture = models.CharField(max_length=25, db_index=True) 374 | machine_os_name = models.CharField(max_length=25, db_index=True) 375 | machine_platform = models.CharField(max_length=100, db_index=True) 376 | machine_architecture = models.CharField(max_length=25, db_index=True) 377 | job_group_name = models.CharField(max_length=100, blank=True, db_index=True) 378 | job_group_symbol = models.CharField(max_length=25, blank=True, db_index=True) 379 | job_type_name = models.CharField(max_length=100, db_index=True) 380 | job_type_symbol = models.CharField(max_length=25, blank=True, db_index=True) 381 | option_collection_hash = models.CharField(max_length=64, blank=True, db_index=True) 382 | build_system_type = models.CharField(max_length=25, blank=True, db_index=True) 383 | repository = models.CharField(max_length=50, db_index=True) 384 | first_submission_timestamp = models.IntegerField(db_index=True) 385 | 386 | class Meta: 387 | db_table = 'reference_data_signatures' 388 | # Remove if/when the model is renamed to 'ReferenceDataSignature'. 389 | verbose_name_plural = 'reference data signatures' 390 | unique_together = ('name', 'signature', 'build_system_type', 'repository') 391 | 392 | 393 | class JobManager(models.Manager): 394 | """ 395 | Convenience functions for operations on groups of jobs 396 | """ 397 | 398 | def cycle_data(self, repository, cycle_interval, chunk_size, sleep_time): 399 | """ 400 | Delete data older than cycle_interval, splitting the target data into 401 | chunks of chunk_size size. Returns the number of result sets deleted 402 | """ 403 | 404 | # Retrieve list of jobs to delete 405 | jobs_max_timestamp = datetime.datetime.now() - cycle_interval 406 | 407 | jobs_cycled = 0 408 | while True: 409 | jobs_chunk = list(self.filter(repository=repository, submit_time__lt=jobs_max_timestamp) 410 | .values_list('guid', flat=True)[:chunk_size]) 411 | 412 | if not jobs_chunk: 413 | # no more jobs to cycle, we're done! 414 | return jobs_cycled 415 | 416 | # Remove ORM entries for these jobs that don't currently have a 417 | # foreign key relation 418 | lines = FailureLine.objects.filter(job_guid__in=jobs_chunk) 419 | 420 | if settings.ELASTICSEARCH_URL: 421 | # To delete the data from elasticsearch we need the document 422 | # id. However selecting all this data can be rather slow, so 423 | # split the job into multiple smaller chunks. 424 | 425 | failures = itertools.chain.from_iterable( 426 | chunked_qs( 427 | lines, 428 | chunk_size=chunk_size, 429 | fields=['id', 'test'], 430 | ), 431 | ) 432 | bulk(failures, action='delete') 433 | 434 | lines.delete() 435 | 436 | # cycle jobs *after* related data has been deleted, to be sure 437 | # we don't have any orphan data 438 | self.filter(guid__in=jobs_chunk).delete() 439 | 440 | jobs_cycled += len(jobs_chunk) 441 | 442 | if sleep_time: 443 | # Allow some time for other queries to get through 444 | time.sleep(sleep_time) 445 | 446 | 447 | class Job(models.Model): 448 | """ 449 | This class represents a build or test job in Treeherder 450 | """ 451 | objects = JobManager() 452 | 453 | id = models.BigAutoField(primary_key=True) 454 | 455 | PENDING = 0 456 | CROSSREFERENCED = 1 457 | AUTOCLASSIFIED = 2 458 | SKIPPED = 3 459 | FAILED = 255 460 | 461 | AUTOCLASSIFY_STATUSES = ((PENDING, 'pending'), 462 | (CROSSREFERENCED, 'crossreferenced'), 463 | (AUTOCLASSIFIED, 'autoclassified'), 464 | (SKIPPED, 'skipped'), 465 | (FAILED, 'failed')) 466 | 467 | repository = models.ForeignKey(Repository, on_delete=models.CASCADE) 468 | guid = models.CharField(max_length=50, unique=True) 469 | project_specific_id = models.PositiveIntegerField(null=True) # unused, see bug 1328985 470 | autoclassify_status = models.IntegerField(choices=AUTOCLASSIFY_STATUSES, default=PENDING) 471 | 472 | # TODO: Remove coalesced_to_guid next time the jobs table is modified (bug 1402992) 473 | coalesced_to_guid = models.CharField(max_length=50, null=True, 474 | default=None) 475 | signature = models.ForeignKey(ReferenceDataSignatures, on_delete=models.CASCADE) 476 | build_platform = models.ForeignKey(BuildPlatform, on_delete=models.CASCADE, related_name='jobs') 477 | machine_platform = models.ForeignKey(MachinePlatform, on_delete=models.CASCADE) 478 | machine = models.ForeignKey(Machine, on_delete=models.CASCADE) 479 | option_collection_hash = models.CharField(max_length=64) 480 | job_type = models.ForeignKey(JobType, on_delete=models.CASCADE, related_name='jobs') 481 | job_group = models.ForeignKey(JobGroup, on_delete=models.CASCADE, related_name='jobs') 482 | product = models.ForeignKey(Product, on_delete=models.CASCADE) 483 | failure_classification = models.ForeignKey(FailureClassification, on_delete=models.CASCADE, related_name='jobs') 484 | who = models.CharField(max_length=50) 485 | reason = models.CharField(max_length=125) 486 | result = models.CharField(max_length=25) 487 | state = models.CharField(max_length=25) 488 | 489 | submit_time = models.DateTimeField() 490 | start_time = models.DateTimeField() 491 | end_time = models.DateTimeField() 492 | last_modified = models.DateTimeField(auto_now=True, db_index=True) 493 | # TODO: Remove next time we add/drop another field. 494 | running_eta = models.PositiveIntegerField(null=True, default=None) 495 | tier = models.PositiveIntegerField() 496 | 497 | push = models.ForeignKey(Push, on_delete=models.CASCADE, related_name='jobs') 498 | 499 | class Meta: 500 | db_table = 'job' 501 | index_together = [ 502 | # these speed up the various permutations of the "similar jobs" 503 | # queries 504 | ('repository', 'job_type', 'start_time'), 505 | ('repository', 'build_platform', 'job_type', 'start_time'), 506 | ('repository', 'option_collection_hash', 'job_type', 'start_time'), 507 | ('repository', 'build_platform', 'option_collection_hash', 508 | 'job_type', 'start_time'), 509 | # this is intended to speed up queries for specific platform / 510 | # option collections on a push 511 | ('machine_platform', 'option_collection_hash', 'push'), 512 | # speed up cycle data 513 | ('repository', 'submit_time'), 514 | ] 515 | 516 | def __str__(self): 517 | return "{0} {1} {2}".format(self.id, self.repository, self.guid) 518 | 519 | def get_platform_option(self, option_collection_map=None): 520 | if not hasattr(self, 'platform_option'): 521 | self.platform_option = '' 522 | option_hash = self.option_collection_hash 523 | if option_hash: 524 | if not option_collection_map: 525 | option_collection_map = OptionCollection.objects.get_option_collection_map() 526 | self.platform_option = option_collection_map.get(option_hash) 527 | 528 | return self.platform_option 529 | 530 | def save(self, *args, **kwargs): 531 | self.last_modified = datetime.datetime.now() 532 | super(Job, self).save(*args, **kwargs) 533 | 534 | def is_fully_autoclassified(self): 535 | """ 536 | Returns whether a job is fully autoclassified (i.e. we have 537 | classification information for all failure lines) 538 | """ 539 | if FailureLine.objects.filter(job_guid=self.guid, 540 | action="truncated").count() > 0: 541 | return False 542 | 543 | classified_error_count = TextLogError.objects.filter( 544 | _metadata__best_classification__isnull=False, 545 | step__job=self).count() 546 | 547 | if classified_error_count == 0: 548 | return False 549 | 550 | from treeherder.model.error_summary import get_filtered_error_lines 551 | 552 | return classified_error_count == len(get_filtered_error_lines(self)) 553 | 554 | def is_fully_verified(self): 555 | """ 556 | Determine if this Job is fully verified based on the state of its Errors. 557 | 558 | An Error (TextLogError or FailureLine) is considered Verified once its 559 | related TextLogErrorMetadata has best_is_verified set to True. A Job 560 | is then considered Verified once all its Errors TextLogErrorMetadata 561 | instances are set to True. 562 | """ 563 | unverified_errors = TextLogError.objects.filter( 564 | _metadata__best_is_verified=False, 565 | step__job=self).count() 566 | 567 | if unverified_errors: 568 | logger.error("Job %r has unverified TextLogErrors", self) 569 | return False 570 | 571 | logger.info("Job %r is fully verified", self) 572 | return True 573 | 574 | def update_after_verification(self, user): 575 | """ 576 | Updates a job's state after being verified by a sheriff 577 | """ 578 | if not self.is_fully_verified(): 579 | return 580 | 581 | classification = 'autoclassified intermittent' 582 | 583 | already_classified = (JobNote.objects.filter(job=self) 584 | .exclude(failure_classification__name=classification) 585 | .exists()) 586 | if already_classified: 587 | # Don't add an autoclassification note if a Human already 588 | # classified this job. 589 | return 590 | 591 | JobNote.create_autoclassify_job_note(job=self, user=user) 592 | 593 | def get_manual_classification_line(self): 594 | """ 595 | Return the TextLogError from a job if it can be manually classified as a side effect 596 | of the overall job being classified. 597 | Otherwise return None. 598 | """ 599 | try: 600 | text_log_error = TextLogError.objects.get(step__job=self) 601 | except (TextLogError.DoesNotExist, TextLogError.MultipleObjectsReturned): 602 | return None 603 | 604 | # Only propagate the classification if there is exactly one unstructured failure 605 | # line for the job 606 | from treeherder.model.error_summary import get_filtered_error_lines 607 | if len(get_filtered_error_lines(self)) != 1: 608 | return None 609 | 610 | # Check that we have a related FailureLine and it's in a state we 611 | # expect for auto-classification. 612 | failure_line = text_log_error.get_failure_line() 613 | if failure_line is None: 614 | return None 615 | 616 | if not (failure_line.action == "test_result" and 617 | failure_line.test and 618 | failure_line.status and 619 | failure_line.expected): 620 | return None 621 | 622 | return text_log_error 623 | 624 | def update_autoclassification_bug(self, bug_number): 625 | text_log_error = self.get_manual_classification_line() 626 | 627 | if text_log_error is None: 628 | return 629 | 630 | classification = (text_log_error.metadata.best_classification if text_log_error.metadata 631 | else None) 632 | if classification and classification.bug_number is None: 633 | return classification.set_bug(bug_number) 634 | 635 | 636 | class TaskclusterMetadata(models.Model): 637 | ''' 638 | Taskcluster-specific metadata associated with a taskcluster job 639 | ''' 640 | job = models.OneToOneField( 641 | Job, 642 | on_delete=models.CASCADE, 643 | primary_key=True, 644 | related_name='taskcluster_metadata' 645 | ) 646 | 647 | task_id = models.CharField(max_length=22, 648 | validators=[MinLengthValidator(22)]) 649 | retry_id = models.PositiveIntegerField() 650 | 651 | class Meta: 652 | db_table = "taskcluster_metadata" 653 | 654 | 655 | @python_2_unicode_compatible 656 | class JobDetail(models.Model): 657 | ''' 658 | Represents metadata associated with a job 659 | 660 | There can be (and usually is) more than one of these associated with 661 | each job 662 | ''' 663 | 664 | id = models.BigAutoField(primary_key=True) 665 | job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="job_details") 666 | title = models.CharField(max_length=70, null=True) 667 | value = models.CharField(max_length=125) 668 | url = models.URLField(null=True, max_length=512) 669 | 670 | class Meta: 671 | db_table = "job_detail" 672 | unique_together = ("title", "value", "job") 673 | 674 | def __str__(self): 675 | return "{0} {1} {2} {3} {4}".format(self.id, 676 | self.job.guid, 677 | self.title, 678 | self.value, 679 | self.url) 680 | 681 | 682 | class JobLog(models.Model): 683 | ''' 684 | Represents a log associated with a job 685 | 686 | There can be more than one of these associated with each job 687 | ''' 688 | PENDING = 0 689 | PARSED = 1 690 | FAILED = 2 691 | 692 | STATUSES = ((PENDING, 'pending'), 693 | (PARSED, 'parsed'), 694 | (FAILED, 'failed')) 695 | 696 | job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="job_log") 697 | name = models.CharField(max_length=50) 698 | url = models.URLField(max_length=255) 699 | status = models.IntegerField(choices=STATUSES, default=PENDING) 700 | 701 | class Meta: 702 | db_table = "job_log" 703 | unique_together = ('job', 'name', 'url') 704 | 705 | def __str__(self): 706 | return "{0} {1} {2} {3}".format(self.id, 707 | self.job.guid, 708 | self.name, 709 | self.status) 710 | 711 | def update_status(self, status): 712 | self.status = status 713 | self.save(update_fields=['status']) 714 | 715 | 716 | class FailuresQuerySet(models.QuerySet): 717 | def default(self, repo, startday, endday): 718 | return self.select_related('push', 'job').filter( 719 | job__repository_id__in=repo, job__push__time__range=(startday, endday)) 720 | 721 | def by_bug(self, bug_id): 722 | return self.filter(bug_id=int(bug_id)) 723 | 724 | 725 | class BugJobMap(models.Model): 726 | ''' 727 | Maps job_ids to related bug_ids 728 | 729 | Mappings can be made manually through a UI or from doing lookups in the 730 | BugsCache 731 | ''' 732 | id = models.BigAutoField(primary_key=True) 733 | 734 | job = models.ForeignKey(Job, on_delete=models.CASCADE) 735 | bug_id = models.PositiveIntegerField(db_index=True) 736 | created = models.DateTimeField(default=timezone.now) 737 | user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) # null if autoclassified 738 | 739 | failures = FailuresQuerySet.as_manager() 740 | objects = models.Manager() 741 | 742 | class Meta: 743 | db_table = "bug_job_map" 744 | unique_together = ('job', 'bug_id') 745 | 746 | @property 747 | def who(self): 748 | if self.user: 749 | return self.user.email 750 | else: 751 | return "autoclassifier" 752 | 753 | def save(self, *args, **kwargs): 754 | super(BugJobMap, self).save(*args, **kwargs) 755 | 756 | # if we have a user, then update the autoclassification relations 757 | if self.user: 758 | self.job.update_autoclassification_bug(self.bug_id) 759 | 760 | def __str__(self): 761 | return "{0} {1} {2} {3}".format(self.id, 762 | self.job.guid, 763 | self.bug_id, 764 | self.user) 765 | 766 | 767 | class JobNote(models.Model): 768 | """ 769 | Associates a Failure type with a Job and optionally a text comment from a User. 770 | 771 | Generally these are generated manually in the UI. 772 | """ 773 | id = models.BigAutoField(primary_key=True) 774 | 775 | job = models.ForeignKey(Job, on_delete=models.CASCADE) 776 | failure_classification = models.ForeignKey(FailureClassification, on_delete=models.CASCADE) 777 | user = models.ForeignKey(User, on_delete=models.CASCADE, null=True) # null if autoclassified 778 | text = models.TextField() 779 | created = models.DateTimeField(default=timezone.now) 780 | 781 | class Meta: 782 | db_table = "job_note" 783 | 784 | @property 785 | def who(self): 786 | if self.user: 787 | return self.user.email 788 | return "autoclassifier" 789 | 790 | def _update_failure_type(self): 791 | """ 792 | Updates the failure type of this Note's Job. 793 | 794 | Set the linked Job's failure type to that of the previous Note or set 795 | to Not Classified if this is the last note. 796 | """ 797 | # update the job classification 798 | note = JobNote.objects.filter(job=self.job).order_by('-created').first() 799 | if note: 800 | self.job.failure_classification_id = note.failure_classification.id 801 | else: 802 | self.job.failure_classification_id = FailureClassification.objects.get(name='not classified').id 803 | self.job.save() 804 | 805 | def _ensure_classification(self): 806 | """ 807 | Ensures a single TextLogError's related bugs have Classifications. 808 | 809 | If the linked Job has a single meaningful TextLogError: 810 | - find the bugs currently related to it via a Classification 811 | - find the bugs mapped to the job related to this note 812 | - find the bugs that are mapped but not classified 813 | - link this subset of bugs to Classifications 814 | - if there's only one new bug and no existing ones, verify it 815 | """ 816 | # if this note was automatically filed, don't update the auto-classification information 817 | if not self.user: 818 | return 819 | 820 | # if the failure type isn't intermittent, ignore 821 | if self.failure_classification.name not in ["intermittent", "intermittent needs filing"]: 822 | return 823 | 824 | # if the linked Job has more than one TextLogError, ignore 825 | text_log_error = self.job.get_manual_classification_line() 826 | if not text_log_error: 827 | return 828 | 829 | # evaluate the QuerySet here so it can be used when creating new_bugs below 830 | existing_bugs = list(ClassifiedFailure.objects.filter(error_matches__text_log_error=text_log_error) 831 | .values_list('bug_number', flat=True)) 832 | 833 | new_bugs = (self.job.bugjobmap_set.exclude(bug_id__in=existing_bugs) 834 | .values_list('bug_id', flat=True)) 835 | 836 | if not new_bugs: 837 | return 838 | 839 | # Create Match instances for each new bug 840 | for bug_number in new_bugs: 841 | classification, _ = ClassifiedFailure.objects.get_or_create(bug_number=bug_number) 842 | text_log_error.create_match("ManualDetector", classification) 843 | 844 | # if there's only one new bug and no existing ones, verify it 845 | if len(new_bugs) == 1 and not existing_bugs: 846 | text_log_error.verify_classification(classification) 847 | 848 | def save(self, *args, **kwargs): 849 | super(JobNote, self).save(*args, **kwargs) 850 | self._update_failure_type() 851 | self._ensure_classification() 852 | 853 | def delete(self, *args, **kwargs): 854 | super(JobNote, self).delete(*args, **kwargs) 855 | self._update_failure_type() 856 | self._ensure_classification() 857 | 858 | def __str__(self): 859 | return "{0} {1} {2} {3}".format(self.id, 860 | self.job.guid, 861 | self.failure_classification, 862 | self.who) 863 | 864 | @classmethod 865 | def create_autoclassify_job_note(self, job, user=None): 866 | """ 867 | Create a JobNote, possibly via auto-classification. 868 | 869 | Create mappings from the given Job to Bugs via verified Classifications 870 | of this Job. 871 | 872 | Also creates a JobNote. 873 | """ 874 | # Only insert bugs for verified failures since these are automatically 875 | # mirrored to ES and the mirroring can't be undone 876 | # TODO: Decide whether this should change now that we're no longer mirroring. 877 | bug_numbers = set(ClassifiedFailure.objects 878 | .filter(best_for_errors__text_log_error__step__job=job, 879 | best_for_errors__best_is_verified=True) 880 | .exclude(bug_number=None) 881 | .exclude(bug_number=0) 882 | .values_list('bug_number', flat=True)) 883 | 884 | for bug_number in bug_numbers: 885 | BugJobMap.objects.get_or_create(job=job, 886 | bug_id=bug_number, 887 | defaults={ 888 | 'user': user 889 | }) 890 | 891 | # if user is not specified, then this is an autoclassified job note and 892 | # we should mark it as such 893 | classification_name = 'intermittent' if user else 'autoclassified intermittent' 894 | classification = FailureClassification.objects.get(name=classification_name) 895 | 896 | return JobNote.objects.create(job=job, 897 | failure_classification=classification, 898 | user=user, 899 | text="") 900 | 901 | 902 | class FailureLine(models.Model): 903 | # We make use of prefix indicies for several columns in this table which 904 | # can't be expressed in django syntax so are created with raw sql in migrations. 905 | STATUS_LIST = ('PASS', 'FAIL', 'OK', 'ERROR', 'TIMEOUT', 'CRASH', 'ASSERT', 'SKIP', 'NOTRUN') 906 | # Truncated is a special action that we use to indicate that the list of failure lines 907 | # was truncated according to settings.FAILURE_LINES_CUTOFF. 908 | ACTION_LIST = ("test_result", "log", "crash", "truncated") 909 | LEVEL_LIST = ("critical", "error", "warning", "info", "debug") 910 | 911 | # Python 3's zip produces an iterable rather than a list, which Django's `choices` can't handle. 912 | ACTION_CHOICES = list(zip(ACTION_LIST, ACTION_LIST)) 913 | STATUS_CHOICES = list(zip(STATUS_LIST, STATUS_LIST)) 914 | LEVEL_CHOICES = list(zip(LEVEL_LIST, LEVEL_LIST)) 915 | 916 | id = models.BigAutoField(primary_key=True) 917 | job_guid = models.CharField(max_length=50) 918 | repository = models.ForeignKey(Repository, on_delete=models.CASCADE) 919 | job_log = models.ForeignKey(JobLog, on_delete=models.CASCADE, null=True, related_name="failure_line") 920 | action = models.CharField(max_length=11, choices=ACTION_CHOICES) 921 | line = models.PositiveIntegerField() 922 | test = models.TextField(blank=True, null=True) 923 | subtest = models.TextField(blank=True, null=True) 924 | status = models.CharField(max_length=7, choices=STATUS_CHOICES) 925 | expected = models.CharField(max_length=7, choices=STATUS_CHOICES, blank=True, null=True) 926 | message = models.TextField(blank=True, null=True) 927 | signature = models.TextField(blank=True, null=True) 928 | level = models.CharField(max_length=8, choices=STATUS_CHOICES, blank=True, null=True) 929 | stack = models.TextField(blank=True, null=True) 930 | stackwalk_stdout = models.TextField(blank=True, null=True) 931 | stackwalk_stderr = models.TextField(blank=True, null=True) 932 | 933 | # Note that the case of best_classification = None and best_is_verified = True 934 | # has the special semantic that the line is ignored and should not be considered 935 | # for future autoclassifications. 936 | best_classification = models.ForeignKey("ClassifiedFailure", 937 | related_name="best_for_lines", 938 | null=True, 939 | db_index=True, 940 | on_delete=models.SET_NULL) 941 | 942 | best_is_verified = models.BooleanField(default=False) 943 | 944 | created = models.DateTimeField(auto_now_add=True) 945 | modified = models.DateTimeField(auto_now=True) 946 | 947 | class Meta: 948 | db_table = 'failure_line' 949 | index_together = ( 950 | ('job_guid', 'repository'), 951 | # Prefix index: test(50), subtest(25), status, expected, created 952 | ('test', 'subtest', 'status', 'expected', 'created'), 953 | # Prefix index: signature(25), test(50), created 954 | ('signature', 'test', 'created') 955 | ) 956 | unique_together = ( 957 | ('job_log', 'line') 958 | ) 959 | 960 | def __str__(self): 961 | return "{0} {1}".format(self.id, Job.objects.get(guid=self.job_guid).id) 962 | 963 | @property 964 | def error(self): 965 | # Return the related text-log-error or None if there is no related field. 966 | try: 967 | return self.text_log_error_metadata.text_log_error 968 | except TextLogErrorMetadata.DoesNotExist: 969 | return None 970 | 971 | def _serialized_components(self): 972 | if self.action == "test_result": 973 | return ["TEST-UNEXPECTED-%s" % self.status.upper(), 974 | self.test] 975 | if self.action == "log": 976 | return [self.level.upper(), 977 | self.message.split("\n")[0]] 978 | 979 | def unstructured_bugs(self): 980 | """ 981 | Get bugs that match this line in the Bug Suggestions artifact for this job. 982 | """ 983 | components = self._serialized_components() 984 | if not components: 985 | return [] 986 | 987 | from treeherder.model.error_summary import get_filtered_error_lines 988 | job = Job.objects.get(guid=self.job_guid) 989 | rv = [] 990 | ids_seen = set() 991 | for item in get_filtered_error_lines(job): 992 | if all(component in item["search"] for component in components): 993 | for suggestion in itertools.chain(item["bugs"]["open_recent"], 994 | item["bugs"]["all_others"]): 995 | if suggestion["id"] not in ids_seen: 996 | ids_seen.add(suggestion["id"]) 997 | rv.append(suggestion) 998 | 999 | return rv 1000 | 1001 | def elastic_search_insert(self): 1002 | if not settings.ELASTICSEARCH_URL: 1003 | return 1004 | 1005 | index(self) 1006 | 1007 | def to_dict(self): 1008 | try: 1009 | metadata = self.text_log_error_metadata 1010 | except ObjectDoesNotExist: 1011 | metadata = None 1012 | 1013 | return { 1014 | 'id': self.id, 1015 | 'job_guid': self.job_guid, 1016 | 'repository': self.repository_id, 1017 | 'job_log': self.job_log_id, 1018 | 'action': self.action, 1019 | 'line': self.line, 1020 | 'test': self.test, 1021 | 'subtest': self.subtest, 1022 | 'status': self.status, 1023 | 'expected': self.expected, 1024 | 'message': self.message, 1025 | 'signature': self.signature, 1026 | 'level': self.level, 1027 | 'stack': self.stack, 1028 | 'stackwalk_stdout': self.stackwalk_stdout, 1029 | 'stackwalk_stderr': self.stackwalk_stderr, 1030 | 'best_classification': metadata.best_classification_id if metadata else None, 1031 | 'best_is_verified': metadata.best_is_verified if metadata else False, 1032 | 'created': self.created, 1033 | 'modified': self.modified, 1034 | } 1035 | 1036 | 1037 | class Group(models.Model): 1038 | """ 1039 | The test harness group. 1040 | 1041 | This is most often a manifest file. But in some instances where a suite 1042 | doesn't have manifests, or a test suite isn't logging its data properly, 1043 | this can simply be "default" 1044 | 1045 | Note: This is not to be confused with JobGroup which is Treeherder specific. 1046 | """ 1047 | id = models.BigAutoField(primary_key=True) 1048 | name = models.CharField(max_length=255, unique=True) 1049 | failure_lines = models.ManyToManyField(FailureLine, 1050 | related_name='group') 1051 | 1052 | def __str__(self): 1053 | return self.name 1054 | 1055 | class Meta: 1056 | db_table = 'group' 1057 | 1058 | 1059 | class ClassifiedFailure(models.Model): 1060 | """ 1061 | Classifies zero or more TextLogErrors as a failure. 1062 | 1063 | Optionally linked to a bug. 1064 | """ 1065 | id = models.BigAutoField(primary_key=True) 1066 | text_log_errors = models.ManyToManyField("TextLogError", through='TextLogErrorMatch', 1067 | related_name='classified_failures') 1068 | # Note that we use a bug number of 0 as a sentinel value to indicate lines that 1069 | # are not actually symptomatic of a real bug, but are still possible to autoclassify 1070 | bug_number = models.PositiveIntegerField(blank=True, null=True, unique=True) 1071 | created = models.DateTimeField(auto_now_add=True) 1072 | modified = models.DateTimeField(auto_now=True) 1073 | 1074 | def __str__(self): 1075 | return "{0} {1}".format(self.id, self.bug_number) 1076 | 1077 | def bug(self): 1078 | # Putting this here forces one query per object; there should be a way 1079 | # to make things more efficient 1080 | return Bugscache.objects.filter(id=self.bug_number).first() 1081 | 1082 | def set_bug(self, bug_number): 1083 | """ 1084 | Set the bug number of this Classified Failure 1085 | 1086 | If an existing ClassifiedFailure exists with the same bug number 1087 | replace this instance with the existing one. 1088 | """ 1089 | if bug_number == self.bug_number: 1090 | return self 1091 | 1092 | other = ClassifiedFailure.objects.filter(bug_number=bug_number).first() 1093 | if not other: 1094 | self.bug_number = bug_number 1095 | self.save(update_fields=['bug_number']) 1096 | return self 1097 | 1098 | self.replace_with(other) 1099 | 1100 | return other 1101 | 1102 | @transaction.atomic 1103 | def replace_with(self, other): 1104 | """ 1105 | Replace this instance with the given other. 1106 | 1107 | Deletes stale Match objects and updates related TextLogErrorMetadatas' 1108 | best_classifications to point to the given other. 1109 | """ 1110 | match_ids_to_delete = list(self.update_matches(other)) 1111 | TextLogErrorMatch.objects.filter(id__in=match_ids_to_delete).delete() 1112 | 1113 | # Update best classifications 1114 | self.best_for_errors.update(best_classification=other) 1115 | 1116 | self.delete() 1117 | 1118 | def update_matches(self, other): 1119 | """ 1120 | Update this instance's Matches to point to the given other's Matches. 1121 | 1122 | Find Matches with the same TextLogError as our Matches, updating their 1123 | score if less than ours and mark our matches for deletion. 1124 | 1125 | If there are no other matches, update ours to point to the other 1126 | ClassifiedFailure. 1127 | """ 1128 | for match in self.error_matches.all(): 1129 | other_matches = TextLogErrorMatch.objects.filter( 1130 | classified_failure=other, 1131 | text_log_error=match.text_log_error, 1132 | ) 1133 | 1134 | if not other_matches: 1135 | match.classified_failure = other 1136 | match.save(update_fields=['classified_failure']) 1137 | continue 1138 | 1139 | # if any of our matches have higher scores than other's matches, 1140 | # overwrite with our score. 1141 | other_matches.filter(score__lt=match.score).update(score=match.score) 1142 | 1143 | yield match.id # for deletion 1144 | 1145 | class Meta: 1146 | db_table = 'classified_failure' 1147 | 1148 | 1149 | class Matcher(models.Model): 1150 | name = models.CharField(max_length=50, unique=True) 1151 | 1152 | class Meta: 1153 | db_table = 'matcher' 1154 | 1155 | 1156 | @python_2_unicode_compatible 1157 | class RunnableJob(models.Model): 1158 | id = models.AutoField(primary_key=True) 1159 | build_platform = models.ForeignKey(BuildPlatform, on_delete=models.CASCADE) 1160 | machine_platform = models.ForeignKey(MachinePlatform, on_delete=models.CASCADE) 1161 | job_type = models.ForeignKey(JobType, on_delete=models.CASCADE) 1162 | job_group = models.ForeignKey(JobGroup, on_delete=models.CASCADE, default=2) 1163 | option_collection_hash = models.CharField(max_length=64) 1164 | ref_data_name = models.CharField(max_length=255) 1165 | build_system_type = models.CharField(max_length=25) 1166 | repository = models.ForeignKey(Repository, on_delete=models.CASCADE) 1167 | last_touched = models.DateTimeField(auto_now=True) 1168 | 1169 | class Meta: 1170 | db_table = 'runnable_job' 1171 | unique_together = ( 1172 | ('ref_data_name', 'build_system_type') 1173 | ) 1174 | 1175 | def __str__(self): 1176 | return "{0} {1} {2}".format(self.id, 1177 | self.ref_data_name, 1178 | self.build_system_type) 1179 | 1180 | 1181 | class TextLogStep(models.Model): 1182 | """ 1183 | An individual step in the textual (unstructured) log 1184 | """ 1185 | id = models.BigAutoField(primary_key=True) 1186 | 1187 | job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="text_log_step") 1188 | 1189 | # these are presently based off of buildbot results 1190 | # (and duplicated in treeherder/etl/buildbot.py) 1191 | SUCCESS = 0 1192 | TEST_FAILED = 1 1193 | BUSTED = 2 1194 | SKIPPED = 3 1195 | EXCEPTION = 4 1196 | RETRY = 5 1197 | USERCANCEL = 6 1198 | UNKNOWN = 7 1199 | SUPERSEDED = 8 1200 | 1201 | RESULTS = ((SUCCESS, 'success'), 1202 | (TEST_FAILED, 'testfailed'), 1203 | (BUSTED, 'busted'), 1204 | (SKIPPED, 'skipped'), 1205 | (EXCEPTION, 'exception'), 1206 | (RETRY, 'retry'), 1207 | (USERCANCEL, 'usercancel'), 1208 | (UNKNOWN, 'unknown'), 1209 | (SUPERSEDED, 'superseded')) 1210 | 1211 | name = models.CharField(max_length=200) 1212 | started = models.DateTimeField(null=True) 1213 | finished = models.DateTimeField(null=True) 1214 | started_line_number = models.PositiveIntegerField() 1215 | finished_line_number = models.PositiveIntegerField() 1216 | result = models.IntegerField(choices=RESULTS) 1217 | 1218 | class Meta: 1219 | db_table = "text_log_step" 1220 | unique_together = ('job', 'started_line_number', 1221 | 'finished_line_number') 1222 | 1223 | 1224 | class TextLogError(models.Model): 1225 | """ 1226 | A detected error line in the textual (unstructured) log 1227 | """ 1228 | id = models.BigAutoField(primary_key=True) 1229 | 1230 | step = models.ForeignKey(TextLogStep, on_delete=models.CASCADE, related_name='errors') 1231 | line = models.TextField() 1232 | line_number = models.PositiveIntegerField() 1233 | 1234 | class Meta: 1235 | db_table = "text_log_error" 1236 | unique_together = ('step', 'line_number') 1237 | 1238 | def __str__(self): 1239 | return "{0} {1}".format(self.id, self.step.job.id) 1240 | 1241 | @property 1242 | def metadata(self): 1243 | try: 1244 | return self._metadata 1245 | except TextLogErrorMetadata.DoesNotExist: 1246 | return None 1247 | 1248 | def bug_suggestions(self): 1249 | from treeherder.model import error_summary 1250 | return error_summary.bug_suggestions_line(self) 1251 | 1252 | def create_match(self, matcher_name, classification): 1253 | """ 1254 | Create a TextLogErrorMatch instance 1255 | 1256 | Typically used for manual "matches" or tests. 1257 | """ 1258 | if classification is None: 1259 | classification = ClassifiedFailure.objects.create() 1260 | 1261 | TextLogErrorMatch.objects.create( 1262 | text_log_error=self, 1263 | classified_failure=classification, 1264 | matcher_name=matcher_name, 1265 | score=1, 1266 | ) 1267 | 1268 | def verify_classification(self, classification): 1269 | """ 1270 | Mark the given ClassifiedFailure as verified. 1271 | 1272 | Handles the classification not currently being related to this 1273 | TextLogError and no Metadata existing. 1274 | """ 1275 | if classification not in self.classified_failures.all(): 1276 | self.create_match("ManualDetector", classification) 1277 | 1278 | # create a TextLogErrorMetadata instance for this TextLogError if it 1279 | # doesn't exist. We can't use update_or_create here since OneToOne 1280 | # relations don't use an object manager so a missing relation is simply 1281 | # None as opposed to RelatedManager. 1282 | if self.metadata is None: 1283 | TextLogErrorMetadata.objects.create(text_log_error=self, 1284 | best_classification=classification, 1285 | best_is_verified=True) 1286 | else: 1287 | self.metadata.best_classification = classification 1288 | self.metadata.best_is_verified = True 1289 | self.metadata.save(update_fields=['best_classification', 'best_is_verified']) 1290 | 1291 | self.metadata.failure_line.elastic_search_insert() 1292 | 1293 | # Send event to NewRelic when a verifing an autoclassified failure. 1294 | match = self.matches.filter(classified_failure=classification).first() 1295 | if not match: 1296 | return 1297 | 1298 | newrelic.agent.record_custom_event('user_verified_classification', { 1299 | 'matcher': match.matcher_name, 1300 | 'job_id': self.id, 1301 | }) 1302 | 1303 | def get_failure_line(self): 1304 | """Get a related FailureLine instance if one exists.""" 1305 | try: 1306 | return self.metadata.failure_line 1307 | except AttributeError: 1308 | return None 1309 | 1310 | 1311 | @python_2_unicode_compatible 1312 | class TextLogErrorMetadata(models.Model): 1313 | """ 1314 | Link matching TextLogError and FailureLine instances. 1315 | 1316 | Tracks best classification and verificiation of a classification. 1317 | 1318 | TODO: Merge into TextLogError. 1319 | """ 1320 | text_log_error = models.OneToOneField(TextLogError, 1321 | primary_key=True, 1322 | related_name="_metadata", 1323 | on_delete=models.CASCADE) 1324 | 1325 | failure_line = models.OneToOneField(FailureLine, 1326 | on_delete=models.CASCADE, 1327 | related_name="text_log_error_metadata", 1328 | null=True) 1329 | 1330 | # Note that the case of best_classification = None and best_is_verified = True 1331 | # has the special semantic that the line is ignored and should not be considered 1332 | # for future autoclassifications. 1333 | best_classification = models.ForeignKey(ClassifiedFailure, 1334 | related_name="best_for_errors", 1335 | null=True, 1336 | on_delete=models.SET_NULL) 1337 | best_is_verified = models.BooleanField(default=False) 1338 | 1339 | class Meta: 1340 | db_table = "text_log_error_metadata" 1341 | 1342 | def __str__(self): 1343 | args = (self.text_log_error_id, self.failure_line_id) 1344 | return 'TextLogError={} FailureLine={}'.format(*args) 1345 | 1346 | 1347 | class TextLogErrorMatch(models.Model): 1348 | """Association table between TextLogError and ClassifiedFailure, containing 1349 | additional data about the association including the matcher that was used 1350 | to create it and a score in the range 0-1 for the goodness of match.""" 1351 | 1352 | id = models.BigAutoField(primary_key=True) 1353 | text_log_error = models.ForeignKey(TextLogError, 1354 | related_name="matches", 1355 | on_delete=models.CASCADE) 1356 | classified_failure = models.ForeignKey(ClassifiedFailure, 1357 | related_name="error_matches", 1358 | on_delete=models.CASCADE) 1359 | 1360 | matcher_name = models.CharField(max_length=255) 1361 | score = models.DecimalField(max_digits=3, decimal_places=2, blank=True, null=True) 1362 | 1363 | class Meta: 1364 | db_table = 'text_log_error_match' 1365 | verbose_name_plural = 'text log error matches' 1366 | unique_together = ( 1367 | ('text_log_error', 'classified_failure', 'matcher_name') 1368 | ) 1369 | 1370 | def __str__(self): 1371 | return "{0} {1}".format( 1372 | self.text_log_error.id, self.classified_failure.id) 1373 | --------------------------------------------------------------------------------