Merge branch 'develop' of github.com:Tegiminis/evennia into develop

# Conflicts:
#	evennia/typeclasses/attributes.py
This commit is contained in:
Tegiminis 2022-07-10 02:34:13 -07:00
commit da0e380fa5
73 changed files with 2774 additions and 1099 deletions

View file

@ -162,6 +162,12 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
way to override features on all ObjectDB-inheriting objects easily.
- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these
data in a similar way to django fields.
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
## Evennia 0.9.5

View file

@ -193,7 +193,7 @@ or in the chat.
[pep8]: http://www.python.org/dev/peps/pep-0008
[pep8tool]: https://pypi.python.org/pypi/pep8
[googlestyle]: http://www.sphinx-doc.org/en/stable/ext/example_google.html
[googlestyle]: https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html
[githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/
[markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting
[command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy

View file

@ -60,22 +60,22 @@ introduction][introduction] to read.
To learn how to get your hands on the code base, the [Getting
started][gettingstarted] page is the way to go. Otherwise you could
browse the [Documentation][wiki] or why not come join the [Evennia
browse the [Documentation][docs] or why not come join the [Evennia
Community forum][group] or join us in our [development chat][chat].
Welcome!
[homepage]: http://www.evennia.com
[gettingstarted]: http://github.com/evennia/evennia/wiki/Getting-Started
[wiki]: https://github.com/evennia/evennia/wiki
[homepage]: https://www.evennia.com
[gettingstarted]: https://www.evennia.com/docs/latest/Getting-Started.html
[docs]: https://www.evennia.com/docs/latest
[screenshot]: https://user-images.githubusercontent.com/294267/30773728-ea45afb6-a076-11e7-8820-49be2168a6b8.png
[logo]: https://github.com/evennia/evennia/blob/master/evennia/web/website/static/website/images/evennia_logo.png
[unittestciimg]: https://github.com/evennia/evennia/workflows/test-suite/badge.svg
[unittestcilink]: https://github.com/evennia/evennia/actions?query=workflow%3Atest-suite
[coverimg]: https://coveralls.io/repos/github/evennia/evennia/badge.svg?branch=master
[coverlink]: https://coveralls.io/github/evennia/evennia?branch=master
[introduction]: https://github.com/evennia/evennia/wiki/Evennia-Introduction
[license]: https://github.com/evennia/evennia/wiki/Licensing
[group]: https://groups.google.com/forum/#!forum/evennia
[chat]: http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb
[introduction]: https://www.evennia.com/docs/latest/Evennia-Introduction.html
[license]: https://www.evennia.com/docs/latest/Licensing.html
[group]: https://github.com/evennia/evennia/discussions
[chat]: https://discord.gg/AJJpcRUhtF
[wikimudpage]: http://en.wikipedia.org/wiki/MUD

View file

@ -152,6 +152,8 @@ mv-local:
@echo "Documentation built (multiversion + autodocs)."
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
# note - don't run deploy/release manually, the result will clash with the
# result of the github actions!
deploy:
make _multiversion-deploy
@echo "Documentation deployed."

View file

@ -17,6 +17,7 @@ git checkout gh-pages
# with the build/ directory available since this is not in git
# remove all but the build dir
# TODO don't delete old branches after 1.0 release; they will get harder and harder to rebuild
ls -Q | grep -v build | xargs rm -Rf
cp -Rf build/html/* .

View file

@ -3,6 +3,7 @@
sphinx==3.2.1
myst-parser==0.15.2
myst-parser[linkify]==0.15.2
Jinja2 < 3.1
# sphinx-multiversion with evennia fixes
git+https://github.com/evennia/sphinx-multiversion.git@evennia-mods#egg=sphinx-multiversion

8
docs/source/.vale.ini Normal file
View file

@ -0,0 +1,8 @@
StylesPath = .vale
Vocab = docs
Packages = write-good
MinAlertLevel = error
[*.md]
BasedOnStyles = Vale, write-good

View file

@ -0,0 +1,19 @@
Evennia
Pastebin
[Cc]ontrib(s)*
Patreon
[Rr]epo(s)*
(?i)readme
[Ss]ubfolder(s)*
[Dd]ev(s)*
Github
[Dd]ocstring(s)*
[Mm]ygame(s)*
[Gg]amedir(s)*
[Vv]irtualenv(s)*
Python
API
[Tt]ypeclass(es)*?
[Bb]ullet point(s)*
CommonMark
[Pp]reparser(s)*

View file

View file

@ -0,0 +1,702 @@
extends: existence
message: "Try to avoid using clichés like '%s'."
ignorecase: true
level: warning
tokens:
- a chip off the old block
- a clean slate
- a dark and stormy night
- a far cry
- a fine kettle of fish
- a loose cannon
- a penny saved is a penny earned
- a tough row to hoe
- a word to the wise
- ace in the hole
- acid test
- add insult to injury
- against all odds
- air your dirty laundry
- all fun and games
- all in a day's work
- all talk, no action
- all thumbs
- all your eggs in one basket
- all's fair in love and war
- all's well that ends well
- almighty dollar
- American as apple pie
- an axe to grind
- another day, another dollar
- armed to the teeth
- as luck would have it
- as old as time
- as the crow flies
- at loose ends
- at my wits end
- avoid like the plague
- babe in the woods
- back against the wall
- back in the saddle
- back to square one
- back to the drawing board
- bad to the bone
- badge of honor
- bald faced liar
- ballpark figure
- banging your head against a brick wall
- baptism by fire
- barking up the wrong tree
- bat out of hell
- be all and end all
- beat a dead horse
- beat around the bush
- been there, done that
- beggars can't be choosers
- behind the eight ball
- bend over backwards
- benefit of the doubt
- bent out of shape
- best thing since sliced bread
- bet your bottom dollar
- better half
- better late than never
- better mousetrap
- better safe than sorry
- between a rock and a hard place
- beyond the pale
- bide your time
- big as life
- big cheese
- big fish in a small pond
- big man on campus
- bigger they are the harder they fall
- bird in the hand
- bird's eye view
- birds and the bees
- birds of a feather flock together
- bit the hand that feeds you
- bite the bullet
- bite the dust
- bitten off more than he can chew
- black as coal
- black as pitch
- black as the ace of spades
- blast from the past
- bleeding heart
- blessing in disguise
- blind ambition
- blind as a bat
- blind leading the blind
- blood is thicker than water
- blood sweat and tears
- blow off steam
- blow your own horn
- blushing bride
- boils down to
- bolt from the blue
- bone to pick
- bored stiff
- bored to tears
- bottomless pit
- boys will be boys
- bright and early
- brings home the bacon
- broad across the beam
- broken record
- brought back to reality
- bull by the horns
- bull in a china shop
- burn the midnight oil
- burning question
- burning the candle at both ends
- burst your bubble
- bury the hatchet
- busy as a bee
- by hook or by crook
- call a spade a spade
- called onto the carpet
- calm before the storm
- can of worms
- can't cut the mustard
- can't hold a candle to
- case of mistaken identity
- cat got your tongue
- cat's meow
- caught in the crossfire
- caught red-handed
- checkered past
- chomping at the bit
- cleanliness is next to godliness
- clear as a bell
- clear as mud
- close to the vest
- cock and bull story
- cold shoulder
- come hell or high water
- cool as a cucumber
- cool, calm, and collected
- cost a king's ransom
- count your blessings
- crack of dawn
- crash course
- creature comforts
- cross that bridge when you come to it
- crushing blow
- cry like a baby
- cry me a river
- cry over spilt milk
- crystal clear
- curiosity killed the cat
- cut and dried
- cut through the red tape
- cut to the chase
- cute as a bugs ear
- cute as a button
- cute as a puppy
- cuts to the quick
- dark before the dawn
- day in, day out
- dead as a doornail
- devil is in the details
- dime a dozen
- divide and conquer
- dog and pony show
- dog days
- dog eat dog
- dog tired
- don't burn your bridges
- don't count your chickens
- don't look a gift horse in the mouth
- don't rock the boat
- don't step on anyone's toes
- don't take any wooden nickels
- down and out
- down at the heels
- down in the dumps
- down the hatch
- down to earth
- draw the line
- dressed to kill
- dressed to the nines
- drives me up the wall
- dull as dishwater
- dyed in the wool
- eagle eye
- ear to the ground
- early bird catches the worm
- easier said than done
- easy as pie
- eat your heart out
- eat your words
- eleventh hour
- even the playing field
- every dog has its day
- every fiber of my being
- everything but the kitchen sink
- eye for an eye
- face the music
- facts of life
- fair weather friend
- fall by the wayside
- fan the flames
- feast or famine
- feather your nest
- feathered friends
- few and far between
- fifteen minutes of fame
- filthy vermin
- fine kettle of fish
- fish out of water
- fishing for a compliment
- fit as a fiddle
- fit the bill
- fit to be tied
- flash in the pan
- flat as a pancake
- flip your lid
- flog a dead horse
- fly by night
- fly the coop
- follow your heart
- for all intents and purposes
- for the birds
- for what it's worth
- force of nature
- force to be reckoned with
- forgive and forget
- fox in the henhouse
- free and easy
- free as a bird
- fresh as a daisy
- full steam ahead
- fun in the sun
- garbage in, garbage out
- gentle as a lamb
- get a kick out of
- get a leg up
- get down and dirty
- get the lead out
- get to the bottom of
- get your feet wet
- gets my goat
- gilding the lily
- give and take
- go against the grain
- go at it tooth and nail
- go for broke
- go him one better
- go the extra mile
- go with the flow
- goes without saying
- good as gold
- good deed for the day
- good things come to those who wait
- good time was had by all
- good times were had by all
- greased lightning
- greek to me
- green thumb
- green-eyed monster
- grist for the mill
- growing like a weed
- hair of the dog
- hand to mouth
- happy as a clam
- happy as a lark
- hasn't a clue
- have a nice day
- have high hopes
- have the last laugh
- haven't got a row to hoe
- head honcho
- head over heels
- hear a pin drop
- heard it through the grapevine
- heart's content
- heavy as lead
- hem and haw
- high and dry
- high and mighty
- high as a kite
- hit paydirt
- hold your head up high
- hold your horses
- hold your own
- hold your tongue
- honest as the day is long
- horns of a dilemma
- horse of a different color
- hot under the collar
- hour of need
- I beg to differ
- icing on the cake
- if the shoe fits
- if the shoe were on the other foot
- in a jam
- in a jiffy
- in a nutshell
- in a pig's eye
- in a pinch
- in a word
- in hot water
- in the gutter
- in the nick of time
- in the thick of it
- in your dreams
- it ain't over till the fat lady sings
- it goes without saying
- it takes all kinds
- it takes one to know one
- it's a small world
- it's only a matter of time
- ivory tower
- Jack of all trades
- jockey for position
- jog your memory
- joined at the hip
- judge a book by its cover
- jump down your throat
- jump in with both feet
- jump on the bandwagon
- jump the gun
- jump to conclusions
- just a hop, skip, and a jump
- just the ticket
- justice is blind
- keep a stiff upper lip
- keep an eye on
- keep it simple, stupid
- keep the home fires burning
- keep up with the Joneses
- keep your chin up
- keep your fingers crossed
- kick the bucket
- kick up your heels
- kick your feet up
- kid in a candy store
- kill two birds with one stone
- kiss of death
- knock it out of the park
- knock on wood
- knock your socks off
- know him from Adam
- know the ropes
- know the score
- knuckle down
- knuckle sandwich
- knuckle under
- labor of love
- ladder of success
- land on your feet
- lap of luxury
- last but not least
- last hurrah
- last-ditch effort
- law of the jungle
- law of the land
- lay down the law
- leaps and bounds
- let sleeping dogs lie
- let the cat out of the bag
- let the good times roll
- let your hair down
- let's talk turkey
- letter perfect
- lick your wounds
- lies like a rug
- life's a bitch
- life's a grind
- light at the end of the tunnel
- lighter than a feather
- lighter than air
- like clockwork
- like father like son
- like taking candy from a baby
- like there's no tomorrow
- lion's share
- live and learn
- live and let live
- long and short of it
- long lost love
- look before you leap
- look down your nose
- look what the cat dragged in
- looking a gift horse in the mouth
- looks like death warmed over
- loose cannon
- lose your head
- lose your temper
- loud as a horn
- lounge lizard
- loved and lost
- low man on the totem pole
- luck of the draw
- luck of the Irish
- make hay while the sun shines
- make money hand over fist
- make my day
- make the best of a bad situation
- make the best of it
- make your blood boil
- man of few words
- man's best friend
- mark my words
- meaningful dialogue
- missed the boat on that one
- moment in the sun
- moment of glory
- moment of truth
- money to burn
- more power to you
- more than one way to skin a cat
- movers and shakers
- moving experience
- naked as a jaybird
- naked truth
- neat as a pin
- needle in a haystack
- needless to say
- neither here nor there
- never look back
- never say never
- nip and tuck
- nip it in the bud
- no guts, no glory
- no love lost
- no pain, no gain
- no skin off my back
- no stone unturned
- no time like the present
- no use crying over spilled milk
- nose to the grindstone
- not a hope in hell
- not a minute's peace
- not in my backyard
- not playing with a full deck
- not the end of the world
- not written in stone
- nothing to sneeze at
- nothing ventured nothing gained
- now we're cooking
- off the top of my head
- off the wagon
- off the wall
- old hat
- older and wiser
- older than dirt
- older than Methuselah
- on a roll
- on cloud nine
- on pins and needles
- on the bandwagon
- on the money
- on the nose
- on the rocks
- on the spot
- on the tip of my tongue
- on the wagon
- on thin ice
- once bitten, twice shy
- one bad apple doesn't spoil the bushel
- one born every minute
- one brick short
- one foot in the grave
- one in a million
- one red cent
- only game in town
- open a can of worms
- open and shut case
- open the flood gates
- opportunity doesn't knock twice
- out of pocket
- out of sight, out of mind
- out of the frying pan into the fire
- out of the woods
- out on a limb
- over a barrel
- over the hump
- pain and suffering
- pain in the
- panic button
- par for the course
- part and parcel
- party pooper
- pass the buck
- patience is a virtue
- pay through the nose
- penny pincher
- perfect storm
- pig in a poke
- pile it on
- pillar of the community
- pin your hopes on
- pitter patter of little feet
- plain as day
- plain as the nose on your face
- play by the rules
- play your cards right
- playing the field
- playing with fire
- pleased as punch
- plenty of fish in the sea
- point with pride
- poor as a church mouse
- pot calling the kettle black
- pretty as a picture
- pull a fast one
- pull your punches
- pulling your leg
- pure as the driven snow
- put it in a nutshell
- put one over on you
- put the cart before the horse
- put the pedal to the metal
- put your best foot forward
- put your foot down
- quick as a bunny
- quick as a lick
- quick as a wink
- quick as lightning
- quiet as a dormouse
- rags to riches
- raining buckets
- raining cats and dogs
- rank and file
- rat race
- reap what you sow
- red as a beet
- red herring
- reinvent the wheel
- rich and famous
- rings a bell
- ripe old age
- ripped me off
- rise and shine
- road to hell is paved with good intentions
- rob Peter to pay Paul
- roll over in the grave
- rub the wrong way
- ruled the roost
- running in circles
- sad but true
- sadder but wiser
- salt of the earth
- scared stiff
- scared to death
- sealed with a kiss
- second to none
- see eye to eye
- seen the light
- seize the day
- set the record straight
- set the world on fire
- set your teeth on edge
- sharp as a tack
- shoot for the moon
- shoot the breeze
- shot in the dark
- shoulder to the wheel
- sick as a dog
- sigh of relief
- signed, sealed, and delivered
- sink or swim
- six of one, half a dozen of another
- skating on thin ice
- slept like a log
- slinging mud
- slippery as an eel
- slow as molasses
- smart as a whip
- smooth as a baby's bottom
- sneaking suspicion
- snug as a bug in a rug
- sow wild oats
- spare the rod, spoil the child
- speak of the devil
- spilled the beans
- spinning your wheels
- spitting image of
- spoke with relish
- spread like wildfire
- spring to life
- squeaky wheel gets the grease
- stands out like a sore thumb
- start from scratch
- stick in the mud
- still waters run deep
- stitch in time
- stop and smell the roses
- straight as an arrow
- straw that broke the camel's back
- strong as an ox
- stubborn as a mule
- stuff that dreams are made of
- stuffed shirt
- sweating blood
- sweating bullets
- take a load off
- take one for the team
- take the bait
- take the bull by the horns
- take the plunge
- takes one to know one
- takes two to tango
- the more the merrier
- the real deal
- the real McCoy
- the red carpet treatment
- the same old story
- there is no accounting for taste
- thick as a brick
- thick as thieves
- thin as a rail
- think outside of the box
- third time's the charm
- this day and age
- this hurts me worse than it hurts you
- this point in time
- three sheets to the wind
- through thick and thin
- throw in the towel
- tie one on
- tighter than a drum
- time and time again
- time is of the essence
- tip of the iceberg
- tired but happy
- to coin a phrase
- to each his own
- to make a long story short
- to the best of my knowledge
- toe the line
- tongue in cheek
- too good to be true
- too hot to handle
- too numerous to mention
- touch with a ten foot pole
- tough as nails
- trial and error
- trials and tribulations
- tried and true
- trip down memory lane
- twist of fate
- two cents worth
- two peas in a pod
- ugly as sin
- under the counter
- under the gun
- under the same roof
- under the weather
- until the cows come home
- unvarnished truth
- up the creek
- uphill battle
- upper crust
- upset the applecart
- vain attempt
- vain effort
- vanquish the enemy
- vested interest
- waiting for the other shoe to drop
- wakeup call
- warm welcome
- watch your p's and q's
- watch your tongue
- watching the clock
- water under the bridge
- weather the storm
- weed them out
- week of Sundays
- went belly up
- wet behind the ears
- what goes around comes around
- what you see is what you get
- when it rains, it pours
- when push comes to shove
- when the cat's away
- when the going gets tough, the tough get going
- white as a sheet
- whole ball of wax
- whole hog
- whole nine yards
- wild goose chase
- will wonders never cease?
- wisdom of the ages
- wise as an owl
- wolf at the door
- words fail me
- work like a dog
- world weary
- worst nightmare
- worth its weight in gold
- wrong side of the bed
- yanking your chain
- yappy as a dog
- years young
- you are what you eat
- you can run but you can't hide
- you only live once
- you're the boss
- young and foolish
- young and vibrant

View file

@ -0,0 +1,32 @@
extends: existence
message: "Try to avoid using '%s'."
ignorecase: true
level: suggestion
tokens:
- am
- are
- aren't
- be
- been
- being
- he's
- here's
- here's
- how's
- i'm
- is
- isn't
- it's
- she's
- that's
- there's
- they're
- was
- wasn't
- we're
- were
- weren't
- what's
- where's
- who's
- you're

View file

@ -0,0 +1,11 @@
extends: repetition
message: "'%s' is repeated!"
level: warning
alpha: true
action:
name: edit
params:
- truncate
- " "
tokens:
- '[^\s]+'

View file

@ -0,0 +1,183 @@
extends: existence
message: "'%s' may be passive voice. Use active voice if you can."
ignorecase: true
level: warning
raw:
- \b(am|are|were|being|is|been|was|be)\b\s*
tokens:
- '[\w]+ed'
- awoken
- beat
- become
- been
- begun
- bent
- beset
- bet
- bid
- bidden
- bitten
- bled
- blown
- born
- bought
- bound
- bred
- broadcast
- broken
- brought
- built
- burnt
- burst
- cast
- caught
- chosen
- clung
- come
- cost
- crept
- cut
- dealt
- dived
- done
- drawn
- dreamt
- driven
- drunk
- dug
- eaten
- fallen
- fed
- felt
- fit
- fled
- flown
- flung
- forbidden
- foregone
- forgiven
- forgotten
- forsaken
- fought
- found
- frozen
- given
- gone
- gotten
- ground
- grown
- heard
- held
- hidden
- hit
- hung
- hurt
- kept
- knelt
- knit
- known
- laid
- lain
- leapt
- learnt
- led
- left
- lent
- let
- lighted
- lost
- made
- meant
- met
- misspelt
- mistaken
- mown
- overcome
- overdone
- overtaken
- overthrown
- paid
- pled
- proven
- put
- quit
- read
- rid
- ridden
- risen
- run
- rung
- said
- sat
- sawn
- seen
- sent
- set
- sewn
- shaken
- shaven
- shed
- shod
- shone
- shorn
- shot
- shown
- shrunk
- shut
- slain
- slept
- slid
- slit
- slung
- smitten
- sold
- sought
- sown
- sped
- spent
- spilt
- spit
- split
- spoken
- spread
- sprung
- spun
- stolen
- stood
- stridden
- striven
- struck
- strung
- stuck
- stung
- stunk
- sung
- sunk
- swept
- swollen
- sworn
- swum
- swung
- taken
- taught
- thought
- thrived
- thrown
- thrust
- told
- torn
- trodden
- understood
- upheld
- upset
- wed
- wept
- withheld
- withstood
- woken
- won
- worn
- wound
- woven
- written
- wrung

View file

@ -0,0 +1,27 @@
Based on [write-good](https://github.com/btford/write-good).
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
```
The MIT License (MIT)
Copyright (c) 2014 Brian Ford
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

View file

@ -0,0 +1,5 @@
extends: existence
message: "Don't start a sentence with '%s'."
level: error
raw:
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'

View file

@ -0,0 +1,6 @@
extends: existence
message: "Don't start a sentence with '%s'."
ignorecase: false
level: error
raw:
- '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b'

View file

@ -0,0 +1,221 @@
extends: existence
message: "'%s' is too wordy."
ignorecase: true
level: warning
tokens:
- a number of
- abundance
- accede to
- accelerate
- accentuate
- accompany
- accomplish
- accorded
- accrue
- acquiesce
- acquire
- additional
- adjacent to
- adjustment
- admissible
- advantageous
- adversely impact
- advise
- aforementioned
- aggregate
- aircraft
- all of
- all things considered
- alleviate
- allocate
- along the lines of
- already existing
- alternatively
- amazing
- ameliorate
- anticipate
- apparent
- appreciable
- as a matter of fact
- as a means of
- as far as I'm concerned
- as of yet
- as to
- as yet
- ascertain
- assistance
- at the present time
- at this time
- attain
- attributable to
- authorize
- because of the fact that
- belated
- benefit from
- bestow
- by means of
- by virtue of
- by virtue of the fact that
- cease
- close proximity
- commence
- comply with
- concerning
- consequently
- consolidate
- constitutes
- demonstrate
- depart
- designate
- discontinue
- due to the fact that
- each and every
- economical
- eliminate
- elucidate
- employ
- endeavor
- enumerate
- equitable
- equivalent
- evaluate
- evidenced
- exclusively
- expedite
- expend
- expiration
- facilitate
- factual evidence
- feasible
- finalize
- first and foremost
- for all intents and purposes
- for the most part
- for the purpose of
- forfeit
- formulate
- have a tendency to
- honest truth
- however
- if and when
- impacted
- implement
- in a manner of speaking
- in a timely manner
- in a very real sense
- in accordance with
- in addition
- in all likelihood
- in an effort to
- in between
- in excess of
- in lieu of
- in light of the fact that
- in many cases
- in my opinion
- in order to
- in regard to
- in some instances
- in terms of
- in the case of
- in the event that
- in the final analysis
- in the nature of
- in the near future
- in the process of
- inception
- incumbent upon
- indicate
- indication
- initiate
- irregardless
- is applicable to
- is authorized to
- is responsible for
- it is
- it is essential
- it seems that
- it was
- magnitude
- maximum
- methodology
- minimize
- minimum
- modify
- monitor
- multiple
- necessitate
- nevertheless
- not certain
- not many
- not often
- not unless
- not unlike
- notwithstanding
- null and void
- numerous
- objective
- obligate
- obtain
- on the contrary
- on the other hand
- one particular
- optimum
- overall
- owing to the fact that
- participate
- particulars
- pass away
- pertaining to
- point in time
- portion
- possess
- preclude
- previously
- prior to
- prioritize
- procure
- proficiency
- provided that
- purchase
- put simply
- readily apparent
- refer back
- regarding
- relocate
- remainder
- remuneration
- requirement
- reside
- residence
- retain
- satisfy
- shall
- should you wish
- similar to
- solicit
- span across
- strategize
- subsequent
- substantial
- successfully complete
- sufficient
- terminate
- the month of
- the point I am trying to make
- therefore
- time period
- took advantage of
- transmit
- transpire
- type of
- until such time as
- utilization
- utilize
- validate
- various different
- what I mean to say is
- whether or not
- with respect to
- with the exception of
- witnessed

View file

@ -0,0 +1,207 @@
extends: existence
message: "'%s' is a weasel word!"
ignorecase: true
level: warning
tokens:
- absolutely
- accidentally
- additionally
- allegedly
- alternatively
- angrily
- anxiously
- approximately
- awkwardly
- badly
- barely
- beautifully
- blindly
- boldly
- bravely
- brightly
- briskly
- bristly
- bubbly
- busily
- calmly
- carefully
- carelessly
- cautiously
- cheerfully
- clearly
- closely
- coldly
- completely
- consequently
- correctly
- courageously
- crinkly
- cruelly
- crumbly
- cuddly
- currently
- daily
- daringly
- deadly
- definitely
- deliberately
- doubtfully
- dumbly
- eagerly
- early
- easily
- elegantly
- enormously
- enthusiastically
- equally
- especially
- eventually
- exactly
- exceedingly
- exclusively
- extremely
- fairly
- faithfully
- fatally
- fiercely
- finally
- fondly
- few
- foolishly
- fortunately
- frankly
- frantically
- generously
- gently
- giggly
- gladly
- gracefully
- greedily
- happily
- hardly
- hastily
- healthily
- heartily
- helpfully
- honestly
- hourly
- hungrily
- hurriedly
- immediately
- impatiently
- inadequately
- ingeniously
- innocently
- inquisitively
- interestingly
- irritably
- jiggly
- joyously
- justly
- kindly
- largely
- lately
- lazily
- likely
- literally
- lonely
- loosely
- loudly
- loudly
- luckily
- madly
- many
- mentally
- mildly
- monthly
- mortally
- mostly
- mysteriously
- neatly
- nervously
- nightly
- noisily
- normally
- obediently
- occasionally
- only
- openly
- painfully
- particularly
- patiently
- perfectly
- politely
- poorly
- powerfully
- presumably
- previously
- promptly
- punctually
- quarterly
- quickly
- quietly
- rapidly
- rarely
- really
- recently
- recklessly
- regularly
- remarkably
- relatively
- reluctantly
- repeatedly
- rightfully
- roughly
- rudely
- sadly
- safely
- selfishly
- sensibly
- seriously
- sharply
- shortly
- shyly
- significantly
- silently
- simply
- sleepily
- slowly
- smartly
- smelly
- smoothly
- softly
- solemnly
- sparkly
- speedily
- stealthily
- sternly
- stupidly
- substantially
- successfully
- suddenly
- surprisingly
- suspiciously
- swiftly
- tenderly
- tensely
- thoughtfully
- tightly
- timely
- truthfully
- unexpectedly
- unfortunately
- usually
- very
- victoriously
- violently
- vivaciously
- warmly
- waverly
- weakly
- wearily
- weekly
- wildly
- wisely
- worldly
- wrinkly
- yearly

View file

@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/write-good/releases.atom",
"vale_version": ">=1.0.0"
}

View file

@ -3,7 +3,7 @@
```{code-block}
:caption: In-game
> set obj/myattr = "test"
```
```
```{code-block} python
:caption: In-code, using the .db wrapper
obj.db.foo = [1, 2, 3, "bar"]
@ -16,8 +16,8 @@ value = attributes.get("myattr", category="bar")
```
```{code-block} python
:caption: In-code, using `AttributeProperty` at class level
from evennia import DefaultObject
from evennia import AttributeProperty
from evennia import DefaultObject
from evennia import AttributeProperty
class MyObject(DefaultObject):
foo = AttributeProperty(default=[1, 2, 3, "bar"])
@ -25,20 +25,20 @@ class MyObject(DefaultObject):
```
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any
Python data structure and data type, like numbers, strings, lists, dicts etc. You can also
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any
Python data structure and data type, like numbers, strings, lists, dicts etc. You can also
store (references to) database objects like characters and rooms.
- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything
but you need to know the quirks.
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
siblings of Attributes.
- [Managing Attributes In-game](#managing-attributes-in-game) for in-game builder commands.
## Managing Attributes in Code
## Managing Attributes in Code
Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities
([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and
Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities
([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and
[Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There
are three ways to manage Attributes, all of which can be mixed.
@ -50,8 +50,8 @@ are three ways to manage Attributes, all of which can be mixed.
The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`)
```python
import evennia
```python
import evennia
obj = evennia.create_object(key="Foo")
@ -64,10 +64,10 @@ obj.db.self_reference = obj # stores a reference to the obj
rose = evennia.search_object(key="rose")[0] # returns a list, grab 0th element
rose.db.has_thorns = True
# retrieving
# retrieving
val1 = obj.db.foo1
val2 = obj.db.foo2
weap = obj.db.weapon
weap = obj.db.weapon
myself = obj.db.self_reference # retrieve reference from db, get object back
is_ouch = rose.db.has_thorns
@ -75,25 +75,25 @@ is_ouch = rose.db.has_thorns
# this will return None, not AttributeError!
not_found = obj.db.jiwjpowiwwerw
# returns all Attributes on the object
obj.db.all
# returns all Attributes on the object
obj.db.all
# delete an Attribute
del obj.db.foo2
```
Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead
you will get `None` back. The special `.db.all` will return a list of all Attributes on
the object. You can replace this with your own Attribute `all` if you want, it will replace the
Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead
you will get `None` back. The special `.db.all` will return a list of all Attributes on
the object. You can replace this with your own Attribute `all` if you want, it will replace the
default `all` functionality until you delete it again.
### Using .attributes
If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
```python
is_ouch = rose.attributes.get("has_thorns")
```python
is_ouch = rose.attributes.get("has_thorns")
obj.attributes.add("helmet", "Knight's helmet")
helmet = obj.attributes.get("helmet")
@ -103,7 +103,7 @@ obj.attributes.add("my game log", "long text about ...")
By using a category you can separate same-named Attributes on the same object to help organization.
```python
```python
# store (let's say we have gold_necklace and ringmail_armor from before)
obj.attributes.add("neck", gold_necklace, category="clothing")
obj.attributes.add("neck", ringmail_armor, category="armor")
@ -113,19 +113,19 @@ neck_clothing = obj.attributes.get("neck", category="clothing")
neck_armor = obj.attributes.get("neck", category="armor")
```
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
Here are the methods of the `AttributeHandler`. See
Here are the methods of the `AttributeHandler`. See
the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details.
- `has(...)` - this checks if the object has an Attribute with this key. This is equivalent
to doing `obj.db.attrname` except you can also check for a specific `category.
- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return
- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return
if the Attribute is not defined (instead of None). By supplying an
`accessing_object` to the call one can also make sure to check permissions before modifying
anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning
`None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store
the Attribute as a raw string rather than to pickle it. While an optimization this should usually
anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning
`None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store
the Attribute as a raw string rather than to pickle it. While an optimization this should usually
not be used unless the Attribute is used for some particular, limited purpose.
- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks.md) can be
supplied here to restrict future access and also the call itself may be checked against locks.
@ -135,30 +135,30 @@ the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for
Examples:
```python
```python
try:
# raise error if Attribute foo does not exist
# raise error if Attribute foo does not exist
val = obj.attributes.get("foo", raise_exception=True):
except AttributeError:
# ...
# return default value if foo2 doesn't exist
val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"])
val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"])
# delete foo if it exists (will silently fail if unset, unless
# raise_exception is set)
obj.attributes.remove("foo")
# view all clothes on obj
all_clothes = obj.attributes.all(category="clothes")
all_clothes = obj.attributes.all(category="clothes")
```
### Using AttributeProperty
### Using AttributeProperty
The third way to set up an Attribute is to use an `AttributeProperty`. This
The third way to set up an Attribute is to use an `AttributeProperty`. This
is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code.
```python
```python
# mygame/typeclasses/characters.py
from evennia import DefaultCharacter
@ -173,16 +173,16 @@ class Character(DefaultCharacter):
sleepy = AttributeProperty(False, autocreate=False)
poisoned = AttributeProperty(False, autocreate=False)
def at_object_creation(self):
# ...
```
def at_object_creation(self):
# ...
```
When a new instance of the class is created, new `Attributes` will be created with the value and category given.
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
```python
```python
char = create_object(Character)
char.strength # returns 10
@ -195,15 +195,15 @@ char.db.sleepy # returns None because autocreate=False (see below)
```
```{warning}
```{warning}
Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors.
```
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
```python
```python
char.sleepy # returns False, no db access
char.db.sleepy # returns None - no Attribute exists
@ -217,39 +217,39 @@ char.sleepy # now returns True, involves db access
```
You can e.g. `del char.strength` to set the value back to the default (the value defined
in the `AttributeProperty`).
You can e.g. `del char.strength` to set the value back to the default (the value defined
in the `AttributeProperty`).
See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions.
## Managing Attributes in-game
Attributes are mainly used by code. But one can also allow the builder to use Attributes to
'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an
Attributes are mainly used by code. But one can also allow the builder to use Attributes to
'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an
enemy NPC to lower its difficuly.
When setting Attributes this way, you are severely limited in what can be stored - this is because
When setting Attributes this way, you are severely limited in what can be stored - this is because
giving players (even builders) the ability to store arbitrary Python would be a severe security
problem.
problem.
In game you can set an Attribute like this:
In game you can set an Attribute like this:
set myobj/foo = "bar"
To view, do
To view, do
set myobj/foo
set myobj/foo
or see them together with all object-info with
or see them together with all object-info with
examine myobj
The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the
The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the
value "bar".
You can store numbers, booleans, strings, tuples, lists and dicts this way. But if
You can store numbers, booleans, strings, tuples, lists and dicts this way. But if
you store a list/tuple/dict they must be proper Python structures and may _only_ contain strings
or numbers. If you try to insert an unsupported structure, the input will be converted to a
or numbers. If you try to insert an unsupported structure, the input will be converted to a
string.
set myobj/mybool = True
@ -263,8 +263,8 @@ For the last line you'll get a warning and the value instead will be saved as a
## Locking and checking Attributes
While the `set` command is limited to builders, individual Attributes are usually not
locked down. You may want to lock certain sensitive Attributes, in particular for games
While the `set` command is limited to builders, individual Attributes are usually not
locked down. You may want to lock certain sensitive Attributes, in particular for games
where you allow player building. You can add such limitations by adding a [lock string](./Locks.md)
to your Attribute. A NAttribute have no locks.
@ -273,7 +273,7 @@ The relevant lock types are
- `attrread` - limits who may read the value of the Attribute
- `attredit` - limits who may set/change this Attribute
You must use the `AttributeHandler` to assign the lockstring to the Attribute:
You must use the `AttributeHandler` to assign the lockstring to the Attribute:
```python
lockstring = "attread:all();attredit:perm(Admins)"
@ -281,7 +281,7 @@ obj.attributes.add("myattr", "bar", lockstring=lockstring)"
```
If you already have an Attribute and want to add a lock in-place you can do so
by having the `AttributeHandler` return the `Attribute` object itself (rather than
by having the `AttributeHandler` return the `Attribute` object itself (rather than
its value) and then assign the lock to it directly:
```python
@ -293,8 +293,8 @@ Note the `return_obj` keyword which makes sure to return the `Attribute` object
could be accessed.
A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes.
To check the `lockstring` you provided, make sure you include `accessing_obj` and set
`default_access=False` as you make a `get` call.
To check the `lockstring` you provided, make sure you include `accessing_obj` and set
`default_access=False` as you make a `get` call.
```python
# in some command code where we want to limit
@ -328,13 +328,13 @@ values into a string representation before storing it to the database. This is d
With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
instances without the `__iter__` method.
* You can generally store any non-iterable Python entity that can be pickled.
* Single database objects/typeclasses can be stored, despite them normally not being possible
to pickle. Evennia wil convert them to an internal representation using their classname,
database-id and creation-date with a microsecond precision. When retrieving, the object
* You can generally store any non-iterable Python entity that can be _pickled_.
* Single database objects/typeclasses can be stored, despite them normally not being possible
to pickle. Evennia will convert them to an internal representation using theihr classname,
database-id and creation-date with a microsecond precision. When retrieving, the object
instance will be re-fetched from the database using this information.
* To convert the database object, Evennia must know it's there. If you *hide* a database object
inside a non-iterable class, you will run into errors - this is not supported!
* If you 'hide' a db-obj as a property on a custom class, Evennia will not be
able to find it to serialize it. For that you need to help it out (see below).
```{code-block} python
:caption: Valid assignments
@ -345,16 +345,55 @@ obj.db.test1 = False
# a database object (will be stored as an internal representation)
obj.db.test2 = myobj
```
As mentioned, Evennia will not be able to automatically serialize db-objects
'hidden' in arbitrary properties on an object. This will lead to an error
when saving the Attribute.
```{code-block} python
:caption: Invalid, 'hidden' dbobject
# example of an invalid, "hidden" dbobject
# example of storing an invalid, "hidden" dbobject in Attribute
class Container:
def __init__(self, mydbobj):
# no way for Evennia to know this is a database object!
self.mydbobj = mydbobj
# let's assume myobj is a db-object
container = Container(myobj)
obj.db.invalid = container # will cause error!
obj.db.mydata = container # will raise error!
```
By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the
object you want to save, you can pre-serialize and post-deserialize all 'hidden'
objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's
[evennia.utils.dbserialize.dbserialize](api:evennia.utils.dbserialize.dbserialize) and
[dbunserialize](api:evennia.utils.dbserialize.dbunserialize) functions to safely
serialize the db-objects you want to store.
```{code-block} python
:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute
from evennia.utils import dbserialize # important
class Container:
def __init__(self, mydbobj):
# A 'hidden' db-object
self.mydbobj = mydbobj
def __serialize_dbobjs__(self):
"""This is called before serialization and allows
us to custom-handle those 'hidden' dbobjs"""
self.mydbobj = dbserialize.dbserialize(self.mydbobj
def __deserialize_dbobjs__(self):
"""This is called after deserialization and allows you to
restore the 'hidden' dbobjs you serialized before"""
self.mydbobj = dbserialize.dbunserialize(self.mydbobj)
# let's assume myobj is a db-object
container = Container(myobj)
obj.db.mydata = container # will now work fine!
```
### Storing multiple objects
@ -404,6 +443,12 @@ obj.db.test8[2]["test"] = 5
# test8 is now [4,2,{"test":5}]
```
Note that if make some advanced iterable object, and store an db-object on it in
a way such that it is _not_ returned by iterating over it, you have created a
'hidden' db-object. See [the previous section](#storing-single-objects) for how
to tell Evennia how to serialize such hidden objects safely.
### Retrieving Mutable objects
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
@ -429,41 +474,41 @@ print(obj.db.mylist) # now also [1, 2, 3, 5]
```
When you extract your mutable Attribute data into a variable like `mylist`, think of it as getting a _snapshot_
of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to
of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to
any other snapshots you may have done previously_.
```python
```python
obj.db.mylist = [1, 2, 3, 4]
mylist1 = obj.db.mylist
mylist2 = obj.db.mylist
mylist1[3] = 5
mylist1 = obj.db.mylist
mylist2 = obj.db.mylist
mylist1[3] = 5
print(mylist1) # this is now [1, 2, 3, 5]
print(obj.db.mylist) # also updated to [1, 2, 3, 5]
print(obj.db.mylist) # also updated to [1, 2, 3, 5]
print(mylist2) # still [1, 2, 3, 4] !
print(mylist2) # still [1, 2, 3, 4] !
```
```{sidebar}
Remember, the complexities of this section only relate to *mutable* iterables - things you can update
in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings,
in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings,
numbers, tuples etc) are already disconnected from the database from the onset.
```
To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save
back the results as needed.
To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save
back the results as needed.
You can also choose to "disconnect" the Attribute entirely from the
database with the help of the `.deserialize()` method:
```python
obj.db.mylist = [1, 2, 3, 4, {1: 2}]
mylist = obj.db.mylist.deserialize()
mylist = obj.db.mylist.deserialize()
```
The result of this operation will be a structure only consisting of normal Python mutables (`list`
instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to
instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to
explicitly save it back to the Attribute for it to save.
## Properties of Attributes
@ -518,7 +563,7 @@ are **non-persistent** - they will _not_ survive a server reload.
Differences between `Attributes` and `NAttributes`:
- `NAttribute`s are always wiped on a server reload.
- They only exist in memory and never involve the database at all, making them faster to
- They only exist in memory and never involve the database at all, making them faster to
access and edit than `Attribute`s.
- `NAttribute`s can store _any_ Python structure (and database object) without limit.
- They can _not_ be set with the standard `set` command (but they are visible with `examine`)
@ -526,10 +571,10 @@ Differences between `Attributes` and `NAttributes`:
There are some important reasons we recommend using `ndb` to store temporary data rather than
the simple alternative of just storing a variable directly on an object:
- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations
the server may do. So using them guarantees that they'll remain available at least as long as
- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations
the server may do. So using them guarantees that they'll remain available at least as long as
the server lives.
- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code
- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code
where it's clear how long-lived (or not) your data is to be.
### Persistent vs non-persistent
@ -557,4 +602,4 @@ useful in a few situations though.
- `NAttribute`s have no restrictions at all on what they can store, since they
don't need to worry about being saved to the database - they work very well for temporary storage.
- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your
grand vision!
grand vision!

View file

@ -10,14 +10,15 @@ the return from the function.
from evennia.utils.funcparser import FuncParser
def _power_callable(*args, **kwargs):
"""This will be callable as $square(number, power=<num>) in string"""
"""This will be callable as $pow(number, power=<num>) in string"""
pow = int(kwargs.get('power', 2))
return float(args[0]) ** pow
# create a parser and tell it that '$pow' means using _power_callable
parser = FuncParser({"pow": _power_callable})
```
Next, just pass a string into the parser, optionally containing `$func(...)` markers:
Next, just pass a string into the parser, containing `$func(...)` markers:
```python
parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
@ -71,7 +72,7 @@ You can apply inline function parsing to any string. The
from evennia.utils import funcparser
parser = FuncParser(callables, **default_kwargs)
parsed_string = parser.parser(input_string, raise_errors=False,
parsed_string = parser.parse(input_string, raise_errors=False,
escape=False, strip=False,
return_str=True, **reserved_kwargs)
@ -90,8 +91,12 @@ available to the parser as you parse strings with it. It can either be
an underscore `_`) will be considered a suitable callable. The name of the function will be the `$funcname`
by which it can be called.
- A `list` of modules/paths. This allows you to pull in modules from many sources for your parsing.
- The `**default` kwargs are optional kwargs that will be passed to _all_
callables every time this parser is used - unless the user overrides it explicitly in
their call. This is great for providing sensible standards that the user can
tweak as needed.
The other arguments to the parser:
`FuncParser.parse` takes further arguments, and can vary for every string parsed.
- `raise_errors` - By default, any errors from a callable will be quietly ignored and the result
will be that the failing function call will show verbatim. If `raise_errors` is set,
@ -102,12 +107,14 @@ The other arguments to the parser:
- `return_str` - When `True` (default), `parser` always returns a string. If `False`, it may return
the return value of a single function call in the string. This is the same as using the `.parse_to_any`
method.
- The `**default/reserved_keywords` are optional and allow you to pass custom data into _every_ function
call. This is great for including things like the current session or config options. Defaults can be
replaced if the user gives the same-named kwarg in the string's function call. Reserved kwargs are always passed,
ignoring defaults or what the user passed. In addition, the `funcparser` and `raise_errors`
reserved kwargs are always passed - the first is a back-reference to the `FuncParser` instance and the second
is the `raise_errors` boolean passed into `FuncParser.parse`.
- The `**reserved_keywords` are _always_ passed to every callable in the string.
They override any `**defaults` given when instantiating the parser and cannot
be overridden by the user - if they enter the same kwarg it will be ignored.
This is great for providing the current session, settings etc.
- The `funcparser` and `raise_errors`
are always added as reserved keywords - the first is a
back-reference to the `FuncParser` instance and the second
is the `raise_errors` boolean given to `FuncParser.parse`.
Here's an example of using the default/reserved keywords:
@ -158,7 +165,8 @@ created the parser.
However, if you _nest_ functions, the return of the innermost function may be something other than
a string. Let's introduce the `$eval` function, which evaluates simple expressions using
Python's `literal_eval` and/or `simple_eval`.
Python's `literal_eval` and/or `simple_eval`. It returns whatever data type it
evaluates to.
"There's a $toint($eval(10 * 2.2))% chance of survival."
@ -177,23 +185,66 @@ will be a string:
"There's a 22% chance of survival."
```
However, if you use the `parse_to_any` (or `parse(..., return_str=True)`) and _don't add any extra string around the outermost function call_,
However, if you use the `parse_to_any` (or `parse(..., return_str=False)`) and
_don't add any extra string around the outermost function call_,
you'll get the return type of the outermost callable back:
```python
parser.parse_to_any("$toint($eval(10 * 2.2)%")
"22%"
parser.parse_to_any("$toint($eval(10 * 2.2)")
22
parser.parse_to_any("the number $toint($eval(10 * 2.2).")
"the number 22"
parser.parse_to_any("$toint($eval(10 * 2.2)%")
"22%"
```
### Escaping special character
When entering funcparser callables in strings, it looks like a regular
function call inside a string:
```python
"This is a $myfunc(arg1, arg2, kwarg=foo)."
```
Commas (`,`) and equal-signs (`=`) are considered to separate the arguments and
kwargs. In the same way, the right parenthesis (`)`) closes the argument list.
Sometimes you want to include commas in the argument without it breaking the
argument list.
```python
"There is a $format(beautiful meadow, with dandelions) to the west."
```
You can escape in various ways.
- Prepending with the escape character `\`
```python
"There is a $format(beautiful meadow\, with dandelions) to the west."
```
- Wrapping your strings in quotes. This works like Python, and you can nest
double and single quotes inside each other if so needed. The result will
be a verbatim string that contains everything but the outermost quotes.
```python
"There is a $format('beautiful meadow, with dandelions') to the west."
```
- If you want verbatim quotes in your string, you can escape them too.
```python
"There is a $format('beautiful meadow, with \'dandelions\'') to the west."
```
### Safe convertion of inputs
Since you don't know in which order users may use your callables, they should always check the types
of its inputs and convert to the type the callable needs. Note also that when converting from strings,
there are limits what inputs you can support. This is because FunctionParser strings are often used by
non-developer players/builders and some things (such as complex classes/callables etc) are just not
safe/possible to convert from string representation.
Since you don't know in which order users may use your callables, they should
always check the types of its inputs and convert to the type the callable needs.
Note also that when converting from strings, there are limits what inputs you
can support. This is because FunctionParser strings can be used by
non-developer players/builders and some things (such as complex
classes/callables etc) are just not safe/possible to convert from string
representation.
In `evennia.utils.utils` is a helper called
[safe_convert_to_types](evennia.utils.utils.safe_convert_to_types). This function
@ -204,19 +255,24 @@ from evennia.utils.utils import safe_convert_to_types
def _process_callable(*args, **kwargs):
"""
A callable with a lot of custom options
$process(expression, local, extra=34, extra2=foo)
$process(expression, local, extra1=34, extra2=foo)
"""
args, kwargs = safe_convert_to_type(
(('py', 'py'), {'extra1': int, 'extra2': str}),
(('py', str), {'extra1': int, 'extra2': str}),
*args, **kwargs)
# args/kwargs should be correct types now
```
In other words, in the callable `$process(expression, local, extra1=..,
extra2=...)`, the first argument will be handled by the 'py' converter
(described below), the second will passed through regular Python `str`,
kwargs will be handled by `int` and `str` respectively. You can supply
your own converter function as long as it takes one argument and returns
the converted result.
In other words,
```python
@ -224,8 +280,7 @@ args, kwargs = safe_convert_to_type(
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
```
Each converter should be a callable taking one argument - this will be the arg/kwarg-value to convert. The
special converter `"py"` will try to convert a string argument to a Python structure with the help of the
The special converter `"py"` will try to convert a string argument to a Python structure with the help of the
following tools (which you may also find useful to experiment with on your own):
- [ast.literal_eval](https://docs.python.org/3.8/library/ast.html#ast.literal_eval) is an in-built Python
@ -339,12 +394,12 @@ references to other objects accessible via these callables.
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
depending on who sees it, and also to reference other people in the same way.
- `$You([key])` - same as `$you` but always capitalized.
- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
- `$conj(verb)` ([code](evennia.utils.funcparser.funcparser_callable_conjugate)) - conjugates a verb
between 2nd person presens to 3rd person presence depending on who
sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](evennia.utils.verb_conjugation)
to do this, and only works for English verbs.
- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
- `$pron(pronoun [,options])` ([code](evennia.utils.funcparser.funcparser_callable_pronoun)) - Dynamically
map pronouns (like his, herself, you, its etc) between 1st/2nd person to 3rd person.
### Example

View file

@ -25,7 +25,7 @@ Evennia depends heavily on good documentation and we are always looking for
extra eyes and hands to improve it. Even small things such as fixing typos are a
great help!
- Easiest is to just [report dicumentation issues][issues] as you find them. If
- Easiest is to just [report documentation issues][issues] as you find them. If
we don't know about them, we can't fix them!
- If you want to help edit the docs directly, [check here](./Contributing-Docs.md)
on how to do it.
@ -83,7 +83,7 @@ like [Pastebin](https://pastebin.com/) and just supply the link.
Evennia has a [contrib](Contribs/Contribs-Overview.md) directory which contains
user-shared code organized by category. You can contribute anything that you
think may be useful to another dev, also highly game-specific code. A contrib
must always be added via a forked respository.
must always be added via a forked repository.
#### Guidelines for making a contrib
@ -101,7 +101,7 @@ must always be added via a forked respository.
documented and part of the installation instructions.
- The contrib must be contained within a separate folder under one of the
contrib categories (`game_systems`, `rpg`, `utils` etc). Ask if you are
unsuare which category to put your contrib under.
unsure which category to put your contrib under.
- The folder (package) should be on the following form:
```

View file

@ -115,7 +115,7 @@ Try to `look` at the box to see the (default) description.
The description you get is not very exciting. Let's add some flavor.
describe box = This is a large and very heavy box.
desc box = This is a large and very heavy box.
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
@ -155,10 +155,10 @@ later, in the [Commands tutorial](./Adding-Commands.md).
[Scripts](../../../Components/Scripts.md) are powerful out-of-character objects useful for many "under the hood" things.
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
on ourselves. There is an example script in `evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py`
that is called `BodyFunctions`. To add this to us we will use the `script` command:
script self = tutorial_examples.bodyfunctions.BodyFunctions
script self = tutorials.bodyfunctions.BodyFunctions
This string will tell Evennia to dig up the Python code at the place we indicate. It already knows
to look in the `contrib/` folder, so we don't have to give the full path.
@ -179,7 +179,7 @@ output every time it fires.
When you are tired of your character's "insights", kill the script with
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
script/stop self = tutorials.bodyfunctions.BodyFunctions
You create your own scripts in Python, outside the game; the path you give to `script` is literally
the Python path to your script file. The [Scripts](../../../Components/Scripts.md) page explains more details.
@ -199,7 +199,7 @@ named simply `Object`. Let's create an object that is a little more interesting.
Let's make us one of _those_!
create/drop button:tutorial_examples.red_button.RedButton
create/drop button:tutorials.red_button.RedButton
The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia
to use for creating the object. There you go - one red button.
@ -301,7 +301,7 @@ The Command-help is something you modify in Python code. We'll get to that when
add Commands. But you can also add regular help entries, for example to explain something about
the history of your game world:
sethelp/add History = At the dawn of time ...
sethelp History = At the dawn of time ...
You will now find your new `History` entry in the `help` list and read your help-text with `help History`.

View file

@ -2,137 +2,130 @@
*A list of resources that may be useful for Evennia users and developers.*
## Official Evennia links
## Official Evennia resources
- [evennia.com](https://www.evennia.com) - Main Evennia portal page. Links to all corners of Evennia.
- [Evennia github page](https://github.com/evennia/evennia) - Download code and read documentation.
- [Evennia official chat
channel](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb)
- Our official IRC chat #evennia at irc.freenode.net:6667.
- [Evennia forums/mailing list](https://groups.google.com/group/evennia) - Web interface to our
google group.
- [Evennia development blog](https://evennia.blogspot.com/) - Musings from the lead developer.
- [Evennia's manual on ReadTheDocs](https://readthedocs.org/projects/evennia/) - Read and download
offline in html, PDF or epub formats.
- [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games.
----
- [Evennia development blog](https://evennia.blogspot.com/) - Musings from the lead developer.
- [Evennia on GitHub](https://github.com/evennia/evennia) - Download code and read documentation.
- [Evennia on Open Hub](https://www.openhub.net/p/6906)
- [Evennia on OpenHatch](https://openhatch.org/projects/Evennia)
- [Evennia on PyPi](https://pypi.python.org/pypi/Evennia-MUD-Server/)
- [Evennia subreddit](https://www.reddit.com/r/Evennia/) (not much there yet though)
## Third-party Evennia utilities and resources
## Evennia Community
*For publicly available games running on Evennia, add and find those in the [Evennia game
index](http://games.evennia.com) instead!*
- [Evennia Game Index](http://games.evennia.com/) - An automated listing of Evennia games.
- [Evennia official Discord channel](https://discord.gg/AJJpcRUhtF)
- [Evennia official forums](https://github.com/evennia/evennia/discussions) on Github Discussions.
- [Evennia subreddit](https://www.reddit.com/r/Evennia/)
- [Discord Evennia channel](https://discord.gg/NecFePw) - This is a fan-driven Discord channel with
a bridge to the official Evennia IRC channel.
## Third-party Evennia tools
---
- [Discord live blog](https://discordapp.com/channels/517176782357528616/517176782781415434) of the
_Blackbirds_ Evennia game project.
- [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin)
an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine.
- [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON
27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an
Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and
challenges [used during the conference](https://dcdark.net/home#).
- [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular
[Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](Arxcode-
installing-help)
- [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your
website.
- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional
coloration for Evennia unit-test output.
- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for
telnet/web).
- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for
Evennia with things like races, combat etc. [Summary
here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/).
- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source
turn-based battle system for Evennia. It also has a [live demo](http://wcb.battlestudio.com/).
- [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people
to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games.
- [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an
older fork of Evennia. It has some specific design goals for building and extending the game based
on input files.
- [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`)
files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia-
- [Discord relay](https://github.com/InspectorCaracal/evennia-things/tree/main/discord_relay) - Two-way chat relays between Evennia channels and Discord channels.
- [docker-compose for Evennia](https://github.com/gtaylor/evennia-docker) - A quick-install setup for running Evennia in a [Docker container](https://www.docker.com/). (See [the official Evennia docs](https://www.evennia.com/docs/latest/Running-Evennia-in-Docker.html) for more details on running Evennia with Docker.)
- [Evcolor](https://github.com/taladan/Pegasus/blob/origin/world/utilities/evcolor) - Optional coloration for Evennia unit-test output.
- [Evennia-wiki](https://github.com/vincent-lg/evennia-wiki) - An Evennia-specific Wiki for your website.
- [nextRPI](https://github.com/cluebyte/nextrpi) - A github project for making a toolbox for people to make [RPI](https://www.topmudsites.com/forums/showthread.php?t=4804)-style Evennia games.
- [Paxboards](https://github.com/aurorachain/paxboards) - Evennia bulletin board system (both for telnet/web).
- [Unreal Engine Evennia plugin](https://www.unrealengine.com/marketplace/en-US/slug/evennia-plugin) an in-progress Unreal plugin for integrating Evennia with Epic Games' Unreal Engine.
- [vim-evennia](https://github.com/amfl/vim-evennia) - A mode for editing batch-build files (`.ev`) files in the [vim](https://www.vim.org/) text editor (Emacs users can use [evennia-
mode.el](https://github.com/evennia/evennia/blob/master/evennia/utils/evennia-mode.el)).
- [The world of Cool battles sources](https://github.com/FlutterSprite/coolbattles) - Open source turn-based battle system for an older version of Evennia.
- [Other Evennia-related repos on github](https://github.com/search?p=1&q=evennia)
----
- [EvCast video series](https://www.youtube.com/playlist?list=PLyYMNttpc-SX1hvaqlUNmcxrhmM64pQXl) -
Tutorial videos explaining installing Evennia, basic Python etc.
- [Evennia-docker](https://github.com/gtaylor/evennia-docker) - Evennia in a [Docker
container](https://www.docker.com/) for quick install and deployment in just a few commands.
- [Evennia's docs in Chinese](http://www.evenniacn.com/) - A translated mirror of a slightly older
Evennia version. Announcement [here](https://groups.google.com/forum/#!topic/evennia/3AXS8ZTzJaA).
- [Evennia for MUSHers](https://musoapbox.net/topic/1150/evennia-for-mushers) - An article describing
Evennia for those used to the MUSH way of doing things.
- *[Language Understanding for Text games using Deep reinforcement
learning](http://news.mit.edu/2015/learning-language-playing-computer-games-0924#_msocom_1)*
([PDF](https://people.csail.mit.edu/karthikn/pdfs/mud-play15.pdf)) - MIT research paper using Evennia
to train AIs.
## Other useful mud development resources
## Evennia-Based Projects
- [ROM area reader](https://github.com/ctoth/area_reader) - Parser for converting ROM area files to
Python objects.
- [Gossip MUD chat network](https://gossip.haus/)
### Code bases
- [Arx sources](https://github.com/Arx-Game/arxcode) - Open-source code release of the very popular [Arx](https://play.arxmush.org/) Evennia game. [Here are instructions for installing](https://www.evennia.com/docs/1.0-dev/Howtos/Arxcode-Installation.html)
- [Encarnia sources](https://github.com/whitehorse-io/encarnia) - An open-sourced game dir for an older version of Evennia with things like races, combat etc. [Summary here](https://www.reddit.com/r/MUD/comments/6z6s3j/encarnia_an_evennia_python_mud_code_base_with/).
- [The dark net/March Hare MUD](https://github.com/thedarknet/evennia) from the 2019 [DEF CON 27](https://www.defcon.org/html/defcon-27/dc-27-index.html) hacker conference in Paris. This is an Evennia game dir with batchcode to build the custom _Hackers_ style cyberspace zone with puzzles and challenges [used during the conference](https://dcdark.net/home#).
- [Muddery](https://github.com/muddery/muddery) - A mud framework under development, based on an older fork of Evennia. It has some specific design goals for building and extending the game based on input files.
## General MUD forums and discussions
### Other
- [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack
channel](https://slack.mudcoders.com/) with discussions on MUD development.
- [MuSoapbox](https://www.musoapbox.net/) - Very active Mu* game community mainly focused on MUSH-type gaming.
- [Imaginary Realities](http://journal.imaginary-realities.com/) - An e-magazine on game and MUD
design that has several articles about Evennia. There is also an
[archive of older issues](http://disinterest.org/resource/imaginary-realities/)
from 1998-2001 that are still very relevant.
- [Optional Realities](http://optionalrealities.com/) - Mud development discussion forums that has
regular articles on MUD development focused on roleplay-intensive games. After a HD crash it's not
as content-rich as it once was.
- [MudLab](http://mudlab.org/) - Mud design discussion forum
- [MudConnector](http://www.mudconnect.com/) - Mud listing and forums
- [MudBytes](http://www.mudbytes.net/) - Mud listing and forums
- [Top Mud Sites](http://www.topmudsites.com/) - Mud listing and forums
- [Planet Mud-Dev](http://planet-muddev.disinterest.org/) - A blog aggregator following blogs of
current MUD development (including Evennia) around the 'net. Worth to put among your RSS
subscriptions.
- Mud Dev mailing list archive ([mirror](http://www.disinterest.org/resource/MUD-Dev/)) -
Influential mailing list active 1996-2004. Advanced game design discussions.
- [Discord live blog](https://discordapp.com/channels/517176782357528616/517176782781415434) of the _Blackbirds_ Evennia game project.
- [Evennia for MUSHers](https://musoapbox.net/topic/1150/evennia-for-mushers) - An article describing Evennia for those used to the MUSH way of doing things.
- *[Language Understanding for Text games using Deep reinforcement learning](http://news.mit.edu/2015/learning-language-playing-computer-games-0924#_msocom_1)*
([PDF](https://people.csail.mit.edu/karthikn/pdfs/mud-play15.pdf)) - MIT research paper using Evennia to train AIs.
----
## General MU* resources
### Tools
- [ROM area reader](https://github.com/ctoth/area_reader) - Parser for converting ROM area files to Python objects.
### Informational
- [Imaginary Realities unofficial archive](http://tharsis-gate.org/articles/imaginary.html) - An e-magazine on game and MUD design that has several articles about Evennia.
- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in particular moo).
- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD telnet protocols.
- [Mud-dev wiki](http://mud-dev.wikidot.com/) - A (very) slowly growing resource on MUD creation.
- [Mud Client/Server Interaction](http://cryosphere.net/mud-protocol.html) - A page on classic MUD
telnet protocols.
- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) -
Greg Taylor gives good advice on mud design.
- [Lost Library of MOO](https://www.hayseed.net/MOO/) - Archive of scientific articles on mudding (in
particular moo).
- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) -
Contains a very useful list of things to think about when starting your new MUD.
- [Lost Garden](http://www.lostgarden.com/) - A game development blog with long and interesting
articles (not MUD-specific)
- [What Games Are](http://whatgamesare.com/) - A blog about general game design (not MUD-specific)
- [The Alexandrian](https://thealexandrian.net/) - A blog about tabletop roleplaying and board games,
but with lots of general discussion about rule systems and game balance that could be applicable
also for MUDs.
- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) -
thought-provoking guidelines and things to think about when designing a virtual multiplayer world
(Raph is known for *Ultima Online* among other things).
- [Mud Tech's fun/cool but ...](https://gc-taylor.com/blog/2013/01/08/mud-tech-funcool-dont-forget-ship-damned-thing/) - Greg Taylor gives good advice on mud design.
- [Nick Gammon's hints thread](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) - Contains a very useful list of things to think about when starting your new MUD.
- [Raph Koster's laws of game design](https://www.raphkoster.com/games/laws-of-online-world-design/the-laws-of-online-world-design/) - thought-provoking guidelines and things to think about when designing a virtual multiplayer world (Raph is known for *Ultima Online* among other things).
## Literature
### Community
- [Grapevine](https://grapevine.haus/) - MUD listings and inter-game chat network
- [MUD Coder's Guild](https://mudcoders.com/) - A blog and [associated Slack channel](https://slack.mudcoders.com/) with discussions on MUD development.
- [MudBytes](http://www.mudbytes.net/) - MUD listing and forums
- [MudConnector](http://www.mudconnect.com/) - MUD listing and forums
- [MudLab](http://mudlab.org/) - MUD design discussion forum
- [MuSoapbox](https://musoapbox.net/) - MU* forum mainly focused on MUSH-type gaming.
- [Top Mud Sites](http://www.topmudsites.com/) - MUD listing and forums
----
## General Game-Dev Resources
### Tools
- [GIT](https://git-scm.com/)
- [Documentation](https://git-scm.com/documentation)
- [Learn GIT in 15 minutes](https://try.github.io/levels/1/challenges/1) (interactive tutorial)
### Frameworks
- [Django's homepage](https://www.djangoproject.com/)
- [Documentation](https://docs.djangoproject.com/en)
- [Code](https://code.djangoproject.com/)
- [Twisted homepage](https://twistedmatrix.com/)
- [Documentation](https://twistedmatrix.com/documents/current/core/howto/index.html)
- [Code](https://twistedmatrix.com/trac/browser)
### Learning Python
- [Python Website](https://www.python.org/)
- [Documentation](https://www.python.org/doc/)
- [Tutorial](https://docs.python.org/tut/tut.html)
- [Library Reference](https://docs.python.org/lib/lib.html)
- [Language Reference](https://docs.python.org/ref/ref.html)
- [Python tips and tricks](https://www.siafoo.net/article/52)
- [Jetbrains Python academy](https://hyperskill.org/onboarding?track=python) - free online programming curriculum for different skill levels
### Blogs
- [Lost Garden](https://lostgarden.home.blog/) - A game development blog with long and interesting articles (not MUD-specific)
- [What Games Are](http://whatgamesare.com/) - A blog about general game design (not MUD-specific)
- [The Alexandrian](https://thealexandrian.net/) - A blog about tabletop roleplaying and board games, but with lots of general discussion about rule systems and game balance that could be applicable also for MUDs.
### Literature
- Richard Bartle *Designing Virtual Worlds*
([amazon page](https://www.amazon.com/Designing-Virtual-Worlds-Richard-Bartle/dp/0131018167)) -
Essential reading for the design of any persistent game
world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as
relevant now as when it came out. Covers everything you need to know and then some.
- Zed A. Shaw *Learn Python the Hard way* ([homepage](https://learnpythonthehardway.org/)) - Despite
the imposing name this book is for the absolute Python/programming beginner. One learns the language
by gradually creating a small text game! It has been used by multiple users before moving on to
Evennia. *Update: This used to be free to read online, this is no longer the case.*
When the rights to Designing Virtual Worlds returned to him, Richard Bartle
made the PDF of his Designing Virtual Worlds freely available through his own
website ([Designing Virtual Worlds](https://mud.co.uk/dvw/)). A direct link to
the PDF can be found [here](https://mud.co.uk/richard/DesigningVirtualWorlds.pdf).
- David M. Beazley *Python Essential Reference (4th ed)*
([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) -
Our recommended book on Python; it not only efficiently summarizes the language but is also
@ -147,29 +140,3 @@ Contains a very useful list of things to think about when starting your new MUD.
economic theory. Written in 1730 but the translation is annotated and the essay is actually very
easy to follow also for a modern reader. Required reading if you think of implementing a sane game
economic system.
## Frameworks
- [Django's homepage](https://www.djangoproject.com/)
- [Documentation](https://docs.djangoproject.com/en)
- [Code](https://code.djangoproject.com/)
- [Twisted homepage](https://twistedmatrix.com/)
- [Documentation](https://twistedmatrix.com/documents/current/core/howto/index.html)
- [Code](https://twistedmatrix.com/trac/browser)
## Tools
- [GIT](https://git-scm.com/)
- [Documentation](https://git-scm.com/documentation)
- [Learn GIT in 15 minutes](https://try.github.io/levels/1/challenges/1) (interactive tutorial)
## Python Info
- [Python Website](https://www.python.org/)
- [Documentation](https://www.python.org/doc/)
- [Tutorial](https://docs.python.org/tut/tut.html)
- [Library Reference](https://docs.python.org/lib/lib.html)
- [Language Reference](https://docs.python.org/ref/ref.html)
- [Python tips and tricks](https://www.siafoo.net/article/52)
- [Jetbrains Python academy](https://hyperskill.org/onboarding?track=python) -
free online programming curriculum for different skill levels

View file

@ -678,7 +678,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
typeclass (str, optional): Typeclass to use for this character. If
not given, use settings.BASE_CHARACTER_TYPECLASS.
permissions (list, optional): If not given, use the account's permissions.
ip (str, optiona): The client IP creating this character. Will fall back to the
ip (str, optional): The client IP creating this character. Will fall back to the
one stored for the account if not given.
kwargs (any): Other kwargs will be used in the create_call.
Returns:
@ -955,7 +955,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
kwargs (any): Other keyword arguments will be added to the
found command object instance as variables before it
executes. This is unused by default Evennia but may be
used to set flags and change operating paramaters for
used to set flags and change operating parameters for
commands at run-time.
"""
@ -1433,7 +1433,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
if _MULTISESSION_MODE == 0:
# in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any
# try to auto-connect to our last connected object, if any
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
@ -1460,7 +1460,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
Called by the login process if a user account is targeted correctly
but provided with an invalid password. By default it does nothing,
but exists to be overriden.
but exists to be overridden.
Args:
session (session): Session logging in.
@ -1703,7 +1703,7 @@ class DefaultGuest(DefaultAccount):
Gets or creates a Guest account object.
Keyword Args:
ip (str, optional): IP address of requestor; used for ban checking,
ip (str, optional): IP address of requester; used for ban checking,
throttling and logging
Returns:

View file

@ -450,9 +450,7 @@ class CmdSetHandler(object):
"""
if "permanent" in kwargs:
logger.log_dep(
"obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'."
)
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'.")
persistent = kwargs["permanent"] if persistent is False else persistent
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):

View file

@ -1071,7 +1071,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
exitname, backshort = self.directions[exitshort]
backname = self.directions[backshort][0]
# if we recieved a typeclass for the exit, add it to the alias(short name)
# if we received a typeclass for the exit, add it to the alias(short name)
if ":" in self.lhs:
# limit to only the first : character
exit_typeclass = ":" + self.lhs.split(":", 1)[-1]
@ -1665,7 +1665,7 @@ class CmdSetAttribute(ObjManipCommand):
def split_nested_attr(self, attr):
"""
Yields tuples of (possible attr name, nested keys on that attr).
For performance, this is biased to the deepest match, but allows compatability
For performance, this is biased to the deepest match, but allows compatibility
with older attrs that might have been named with `[]`'s.
> list(split_nested_attr("nested['asdf'][0]"))
@ -2219,11 +2219,13 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
old_typeclass_path = obj.typeclass_path
if reset:
answer = yield("|yNote that this will reset the object back to its typeclass' default state, "
"removing any custom locks/perms/attributes etc that may have been added "
"by an explicit create_object call. Use `update` or type/force instead in order "
"to keep such data. "
"Continue [Y]/N?|n")
answer = yield (
"|yNote that this will reset the object back to its typeclass' default state, "
"removing any custom locks/perms/attributes etc that may have been added "
"by an explicit create_object call. Use `update` or type/force instead in order "
"to keep such data. "
"Continue [Y]/N?|n"
)
if answer.upper() in ("N", "NO"):
caller.msg("Aborted.")
return
@ -2732,7 +2734,7 @@ class CmdExamine(ObjManipCommand):
return
if ndb_attr and ndb_attr[0]:
return "\n " + " \n".join(
return "\n " + "\n ".join(
sorted(self.format_single_attribute(attr) for attr in ndb_attr)
)
@ -2830,7 +2832,7 @@ class CmdExamine(ObjManipCommand):
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[
f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"
f"Commands available to {obj.key} (result of Merged Cmdset(s))"
] = self.format_current_cmds(obj, current_cmdset)
if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj)
@ -3473,7 +3475,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
caller.msg("\n".join(msgs))
if "delete" not in self.switches:
if script and script.pk:
ScriptEvMore(caller, [script], session=self.session)
ScriptEvMore(caller, [script], session=self.session)
else:
caller.msg("Script was deleted automatically.")
else:
@ -4029,7 +4031,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
)
return
try:
# we homogenize the protoype first, to be more lenient with free-form
# we homogenize the prototype first, to be more lenient with free-form
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
except RuntimeError as err:
self.caller.msg(str(err))

View file

@ -1818,7 +1818,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
"""
Link an Evennia channel to an exteral Grapevine channel
Link an Evennia channel to an external Grapevine channel
Usage:
grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>

View file

@ -67,7 +67,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
help <topic>/<subtopic>/<subsubtopic> ...
Use the 'help' command alone to see an index of all help topics, organized
by category.eSome big topics may offer additional sub-topics.
by category. Some big topics may offer additional sub-topics.
"""
@ -138,7 +138,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
click_topics=True,
):
"""This visually formats the help entry.
This method can be overriden to customize the way a help
This method can be overridden to customize the way a help
entry is displayed.
Args:

View file

@ -107,8 +107,7 @@ class TestGeneral(BaseEvenniaCommandTest):
def test_nick_list(self):
self.call(general.CmdNick(), "/list", "No nicks defined.")
self.call(general.CmdNick(), "test1 = Hello",
"Inputline-nick 'test1' mapped to 'Hello'.")
self.call(general.CmdNick(), "test1 = Hello", "Inputline-nick 'test1' mapped to 'Hello'.")
self.call(general.CmdNick(), "/list", "Defined Nicks:")
def test_get_and_drop(self):
@ -1295,7 +1294,8 @@ class TestBuilding(BaseEvenniaCommandTest):
"Obj2 = evennia.objects.objects.DefaultExit",
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
"to evennia.objects.objects.DefaultExit.",
cmdstring="swap", inputs=["yes"],
cmdstring="swap",
inputs=["yes"],
)
self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
self.call(
@ -1332,7 +1332,7 @@ class TestBuilding(BaseEvenniaCommandTest):
"/reset/force Obj=evennia.objects.objects.DefaultObject",
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
"All object creation hooks were run. All old attributes where deleted before the swap.",
inputs=["yes"]
inputs=["yes"],
)
from evennia.prototypes.prototypes import homogenize_prototype
@ -1359,7 +1359,7 @@ class TestBuilding(BaseEvenniaCommandTest):
"typeclasses.objects.Object.\nOnly the at_object_creation hook was run "
"(update mode). Attributes set before swap were not removed\n"
"(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was "
"successfully applied over the object type."
"successfully applied over the object type.",
)
assert self.obj1.db.desc == "protdesc"

View file

@ -17,8 +17,10 @@ def get_component_class(component_name):
subclasses = Component.__subclasses__()
component_class = next((sc for sc in subclasses if sc.name == component_name), None)
if component_class is None:
message = f"Component named {component_name} has not been found. " \
f"Make sure it has been imported before being used."
message = (
f"Component named {component_name} has not been found. "
f"Make sure it has been imported before being used."
)
raise Exception(message)
return component_class

View file

@ -13,6 +13,7 @@ class Component:
Each Component must supply the name, it is used as a slot name but also part of the attribute key.
"""
name = ""
def __init__(self, host=None):

View file

@ -26,7 +26,7 @@ class DBField(AttributeProperty):
db_fields = getattr(owner, "_db_fields", None)
if db_fields is None:
db_fields = {}
setattr(owner, '_db_fields', db_fields)
setattr(owner, "_db_fields", db_fields)
db_fields[name] = self
@ -50,7 +50,7 @@ class NDBField(NAttributeProperty):
ndb_fields = getattr(owner, "_ndb_fields", None)
if ndb_fields is None:
ndb_fields = {}
setattr(owner, '_ndb_fields', ndb_fields)
setattr(owner, "_ndb_fields", ndb_fields)
ndb_fields[name] = self
@ -64,6 +64,7 @@ class TagField:
Default value of a tag is added when the component is registered.
Tags are removed if the component itself is removed.
"""
def __init__(self, default=None, enforce_single=False):
self._category_key = None
self._default = default
@ -78,7 +79,7 @@ class TagField:
tag_fields = getattr(owner, "_tag_fields", None)
if tag_fields is None:
tag_fields = {}
setattr(owner, '_tag_fields', tag_fields)
setattr(owner, "_tag_fields", tag_fields)
tag_fields[name] = self
def __get__(self, instance, owner):

View file

@ -16,6 +16,7 @@ class ComponentProperty:
Defaults can be overridden for this typeclass by passing kwargs
"""
def __init__(self, component_name, **kwargs):
"""
Initializes the descriptor
@ -49,6 +50,7 @@ class ComponentHandler:
It lets you add or remove components and will load components as needed.
It stores the list of registered components on the host .db with component_names as key.
"""
def __init__(self, host):
self.host = host
self._loaded_components = {}
@ -124,7 +126,9 @@ class ComponentHandler:
self.host.signals.remove_object_listeners_and_responders(component)
del self._loaded_components[component_name]
else:
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
message = (
f"Cannot remove {component_name} from {self.host.name} as it is not registered."
)
raise ComponentIsNotRegistered(message)
def remove_by_name(self, name):
@ -199,7 +203,9 @@ class ComponentHandler:
self._set_component(component_instance)
self.host.signals.add_object_listeners_and_responders(component_instance)
else:
message = f"Could not initialize runtime component {component_name} of {self.host.name}"
message = (
f"Could not initialize runtime component {component_name} of {self.host.name}"
)
raise ComponentDoesNotExist(message)
def _set_component(self, component):

View file

@ -15,9 +15,11 @@ def as_listener(func=None, signal_name=None):
signal_name (str): The name of the signal to listen to, defaults to function name.
"""
if not func and signal_name:
def wrapper(func):
func._listener_signal_name = signal_name
return func
return wrapper
signal_name = func.__name__
@ -35,9 +37,11 @@ def as_responder(func=None, signal_name=None):
signal_name (str): The name of the signal to respond to, defaults to function name.
"""
if not func and signal_name:
def wrapper(func):
func._responder_signal_name = signal_name
return func
return wrapper
signal_name = func.__name__
@ -177,12 +181,12 @@ class SignalsHandler(object):
"""
type_host = type(obj)
for att_name, att_obj in type_host.__dict__.items():
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name:
callback = getattr(obj, att_name)
self.add_listener(signal_name=listener_signal_name, callback=callback)
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
if responder_signal_name:
callback = getattr(obj, att_name)
self.add_responder(signal_name=responder_signal_name, callback=callback)
@ -196,12 +200,12 @@ class SignalsHandler(object):
"""
type_host = type(obj)
for att_name, att_obj in type_host.__dict__.items():
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name:
callback = getattr(obj, att_name)
self.remove_listener(signal_name=listener_signal_name, callback=callback)
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
if responder_signal_name:
callback = getattr(obj, att_name)
self.remove_responder(signal_name=responder_signal_name, callback=callback)

View file

@ -56,7 +56,7 @@ class TestComponents(EvenniaTest):
def test_character_can_register_runtime_component(self):
rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct)
test_c = self.char1.components.get('test_c')
test_c = self.char1.components.get("test_c")
assert test_c
assert test_c.my_int == 6
@ -110,7 +110,7 @@ class TestComponents(EvenniaTest):
assert handler.get("test_c") is rct
def test_can_access_component_regular_get(self):
assert self.char1.cmp.test_a is self.char1.components.get('test_a')
assert self.char1.cmp.test_a is self.char1.components.get("test_a")
def test_returns_none_with_regular_get_when_no_attribute(self):
assert self.char1.cmp.does_not_exist is None
@ -127,7 +127,7 @@ class TestComponents(EvenniaTest):
def test_host_has_added_component_tags(self):
rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct)
test_c = self.char1.components.get('test_c')
test_c = self.char1.components.get("test_c")
assert self.char1.tags.has(key="test_c", category="components")
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
@ -162,7 +162,7 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
def test_component_tags_only_hold_one_value_when_enforce_single(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.single_tag = "first_value"
test_b.single_tag = "second value"
@ -171,7 +171,7 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="first_value", category="test_b::single_tag")
def test_component_tags_default_value_is_overridden_when_enforce_single(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.default_single_tag = "second value"
assert self.char1.tags.has(key="second value", category="test_b::default_single_tag")
@ -179,12 +179,14 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag")
def test_component_tags_support_multiple_values_by_default(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.multiple_tags = "first value"
test_b.multiple_tags = "second value"
test_b.multiple_tags = "third value"
assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value"))
assert all(
val in test_b.multiple_tags for val in ("first value", "second value", "third value")
)
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")
@ -193,11 +195,11 @@ class TestComponents(EvenniaTest):
class CharWithSignal(ComponentHolderMixin, DefaultCharacter):
@signals.as_listener
def my_signal(self):
setattr(self, 'my_signal_is_called', True)
setattr(self, "my_signal_is_called", True)
@signals.as_listener
def my_other_signal(self):
setattr(self, 'my_other_signal_is_called', True)
setattr(self, "my_other_signal_is_called", True)
@signals.as_responder
def my_response(self):
@ -213,11 +215,11 @@ class ComponentWithSignal(Component):
@signals.as_listener
def my_signal(self):
setattr(self, 'my_signal_is_called', True)
setattr(self, "my_signal_is_called", True)
@signals.as_listener
def my_other_signal(self):
setattr(self, 'my_other_signal_is_called', True)
setattr(self, "my_other_signal_is_called", True)
@signals.as_responder
def my_response(self):
@ -236,14 +238,15 @@ class TestComponentSignals(BaseEvenniaTest):
def setUp(self):
super().setUp()
self.char1 = create.create_object(
CharWithSignal, key="Char",
CharWithSignal,
key="Char",
)
def test_host_can_register_as_listener(self):
self.char1.signals.trigger("my_signal")
assert self.char1.my_signal_is_called
assert not getattr(self.char1, 'my_other_signal_is_called', None)
assert not getattr(self.char1, "my_other_signal_is_called", None)
def test_host_can_register_as_responder(self):
responses = self.char1.signals.query("my_response")
@ -258,7 +261,7 @@ class TestComponentSignals(BaseEvenniaTest):
component = char.cmp.test_signal_a
assert component.my_signal_is_called
assert not getattr(component, 'my_other_signal_is_called', None)
assert not getattr(component, "my_other_signal_is_called", None)
def test_component_can_register_as_responder(self):
char = self.char1

View file

@ -328,4 +328,4 @@ class GametimeScript(DefaultScript):
callback()
seconds = real_seconds_until(**self.db.gametime)
self.restart(interval=seconds)
self.start(interval=seconds, force_restart=True)

View file

@ -4,7 +4,6 @@ Roleplaying emotes and language - Griatch, 2015
"""
from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa
from .rpsystem import ordered_permutation_regex, regex_tuple_from_key_alias # noqa
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
from .rpsystem import SdescHandler, RecogHandler # noqa
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa

File diff suppressed because it is too large Load diff

View file

@ -96,7 +96,7 @@ recog01 = "Mr Receiver"
recog02 = "Mr Receiver2"
recog10 = "Mr Sender"
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice."
case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
class TestRPSystem(BaseEvenniaTest):
@ -113,41 +113,11 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
)
def test_ordered_permutation_regex(self):
self.assertEqual(
rpsystem.ordered_permutation_regex(sdesc0),
"/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|"
"/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|"
"/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender(?=\\W|$)+|"
"/[0-9]*-*of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*sender\\ of(?=\\W|$)+|"
"/[0-9]*-*A\\ nice(?=\\W|$)+|"
"/[0-9]*-*emotes(?=\\W|$)+|"
"/[0-9]*-*sender(?=\\W|$)+|"
"/[0-9]*-*nice(?=\\W|$)+|"
"/[0-9]*-*of(?=\\W|$)+|"
"/[0-9]*-*A(?=\\W|$)+",
)
def test_sdesc_handler(self):
self.speaker.sdesc.add(sdesc0)
self.assertEqual(self.speaker.sdesc.get(), sdesc0)
self.speaker.sdesc.add("This is {#324} ignored")
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
self.speaker.sdesc.add("Testing three words")
self.assertEqual(
self.speaker.sdesc.get_regex_tuple()[0].pattern,
"/[0-9]*-*Testing\ three\ words(?=\W|$)+|"
"/[0-9]*-*Testing\ three(?=\W|$)+|"
"/[0-9]*-*three\ words(?=\W|$)+|"
"/[0-9]*-*Testing(?=\W|$)+|"
"/[0-9]*-*three(?=\W|$)+|"
"/[0-9]*-*words(?=\W|$)+",
)
def test_recog_handler(self):
self.speaker.sdesc.add(sdesc0)
@ -156,12 +126,8 @@ class TestRPSystem(BaseEvenniaTest):
self.speaker.recog.add(self.receiver2, recog02)
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
self.assertEqual(
self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern,
"/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+",
)
self.speaker.recog.remove(self.receiver1)
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1)
self.assertEqual(self.speaker.recog.get(self.receiver1), None)
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
@ -198,6 +164,26 @@ class TestRPSystem(BaseEvenniaTest):
result,
)
def test_get_sdesc(self):
looker = self.speaker # Sender
target = self.receiver1 # Receiver1
looker.sdesc.add(sdesc0) # A nice sender of emotes
target.sdesc.add(sdesc1) # The first receiver of emotes.
# sdesc with no processing
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
# sdesc with processing
self.assertEqual(
looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n"
)
looker.recog.add(target, recog01) # Mr Receiver
# recog with no processing
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
# recog with processing
self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")
def test_send_emote(self):
speaker = self.speaker
receiver1 = self.receiver1
@ -212,18 +198,18 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
self.assertEqual(
self.out0,
"With a flair, |bSender|n looks at |bThe first receiver of emotes.|n "
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
)
self.assertEqual(
self.out1,
"With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and "
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
)
self.assertEqual(
self.out2,
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n',
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
)
def test_send_case_sensitive_emote(self):
@ -241,20 +227,21 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, case_emote)
self.assertEqual(
self.out0,
"|bSender|n looks at |bthe first receiver of emotes.|n, then "
"|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and "
"|bAnother nice colliding sdesc-guy for tests|n twice.",
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out1,
"|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, "
"|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.",
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out2,
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, "
"then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of "
"emotes.|n and |bReceiver2|n twice.",
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
"|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
)
def test_rpsearch(self):
@ -265,18 +252,6 @@ class TestRPSystem(BaseEvenniaTest):
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
def test_regex_tuple_from_key_alias(self):
self.speaker.aliases.add("foo bar")
self.speaker.aliases.add("this thing is a long thing")
t0 = time.time()
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t1 = time.time()
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t2 = time.time()
# print(f"t1: {t1 - t0}, t2: {t2 - t1}")
self.assertLess(t2 - t1, 10**-4)
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
class TestRPSystemCommands(BaseEvenniaCommandTest):
def setUp(self):
@ -305,7 +280,7 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call(
rpsystem.CmdRecog(),
"barfoo as friend",
"Char will now remember BarFoo Character as friend.",
"You will now remember BarFoo Character as friend.",
)
self.call(
rpsystem.CmdRecog(),
@ -316,6 +291,6 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call(
rpsystem.CmdRecog(),
"friend",
"Char will now know them only as 'BarFoo Character'",
"You will now know them only as 'BarFoo Character'",
cmdstring="forget",
)

View file

@ -321,7 +321,6 @@ class TestTraitStatic(_TraitHandlerBase):
self.trait.mult = 0.75
self.assertEqual(self._get_values(), (5, 1, 0.75, 4.5))
def test_delete(self):
"""Deleting resets to default."""
self.trait.mult = 2.0
@ -362,7 +361,14 @@ class TestTraitCounter(_TraitHandlerBase):
def _get_values(self):
"""Get (base, mod, mult, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max)
return (
self.trait.base,
self.trait.mod,
self.trait.mult,
self.trait.value,
self.trait.min,
self.trait.max,
)
def test_init(self):
self.assertEqual(
@ -634,7 +640,14 @@ class TestTraitGauge(_TraitHandlerBase):
def _get_values(self):
"""Get (base, mod, mult, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max)
return (
self.trait.base,
self.trait.mod,
self.trait.mult,
self.trait.value,
self.trait.min,
self.trait.max,
)
def test_init(self):
self.assertEqual(

View file

@ -1148,7 +1148,7 @@ class Trait:
class StaticTrait(Trait):
"""
Static Trait. This is a single value with a modifier,
Static Trait. This is a single value with a modifier,
multiplier, and no concept of a 'current' value or min/max etc.
value = (base + mod) * mult
@ -1161,7 +1161,9 @@ class StaticTrait(Trait):
def __str__(self):
status = "{value:11}".format(value=self.value)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
name=self.name, status=status, mod=self.mod, mult=self.mult
)
# Helpers
@property
@ -1189,7 +1191,7 @@ class StaticTrait(Trait):
def mult(self):
"""The trait's multiplier."""
return self._data["mult"]
@mult.setter
def mult(self, amount):
if type(amount) in (int, float):
@ -1322,16 +1324,16 @@ class CounterTrait(Trait):
now = time()
tdiff = now - self._data["last_update"]
current += rate * tdiff
value = (current + self.mod)
value = current + self.mod
# we must make sure so we don't overstep our bounds
# even if .mod is included
if self._passed_ratetarget(value):
current = (self._data["ratetarget"] - self.mod)
current = self._data["ratetarget"] - self.mod
self._stop_timer()
elif not self._within_boundaries(value):
current = (self._enforce_boundaries(value) - self.mod)
current = self._enforce_boundaries(value) - self.mod
self._stop_timer()
else:
self._data["last_update"] = now
@ -1378,7 +1380,7 @@ class CounterTrait(Trait):
@property
def mult(self):
return self._data["mult"]
@mult.setter
def mult(self, amount):
if type(amount) in (int, float):
@ -1571,7 +1573,9 @@ class GaugeTrait(CounterTrait):
def __str__(self):
status = "{value:4} / {base:4}".format(value=self.value, base=self.base)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
name=self.name, status=status, mod=self.mod, mult=self.mult
)
@property
def base(self):
@ -1596,11 +1600,11 @@ class GaugeTrait(CounterTrait):
if value + self.base < self.min:
value = self.min - self.base
self._data["mod"] = value
@property
def mult(self):
return self._data["mult"]
@mult.setter
def mult(self, amount):
if type(amount) in (int, float):
@ -1621,7 +1625,7 @@ class GaugeTrait(CounterTrait):
if value is None:
self._data["min"] = self.default_keys["min"]
elif type(value) in (int, float):
self._data["min"] = min(value, (self.base + self.mod) * self.mult)
self._data["min"] = min(value, (self.base + self.mod) * self.mult)
@property
def max(self):
@ -1644,7 +1648,7 @@ class GaugeTrait(CounterTrait):
def current(self):
"""The `current` value of the gauge."""
return self._update_current(
self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
)
@current.setter
@ -1655,7 +1659,7 @@ class GaugeTrait(CounterTrait):
@current.deleter
def current(self):
"Resets current back to 'full'"
self._data["current"] = (self.base + self.mod) * self.mult
self._data["current"] = (self.base + self.mod) * self.mult
@property
def value(self):

View file

@ -1,6 +1,6 @@
# world/
This folder is meant as a miscellanous folder for all that other stuff
This folder is meant as a miscellaneous folder for all that other stuff
related to the game. Code which are not commands or typeclasses go
here, like custom economy systems, combat code, batch-files etc.

View file

@ -28,7 +28,7 @@ Possible keywords are:
- `prototype_key` - the name of the prototype. This is required for db-prototypes,
for module-prototypes, the global variable name of the dict is used instead
- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
in a similar way as classes, with children overriding values in their partents.
in a similar way as classes, with children overriding values in their parents.
- `key` - string, the main object identifier.
- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
- `location` - this should be a valid object or #dbref.
@ -42,7 +42,7 @@ Possible keywords are:
of the shorter forms, defaults are used for the rest.
- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
- Any other keywords are interpreted as Attributes with no category or lock.
These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
These will internally be added to `attrs` (equivalent to `(attrname, value)`.
See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.

View file

@ -12,9 +12,13 @@ import re
# since we use them (e.g. as command names).
# Lunr's default ignore-word list is found here:
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = (
["about", "might", "get", "who", "say"] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
)
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = [
"about",
"might",
"get",
"who",
"say",
] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
_LUNR = None

View file

@ -473,6 +473,7 @@ def tag(accessing_obj, accessed_obj, *args, **kwargs):
category = args[1] if len(args) > 1 else None
return bool(accessing_obj.tags.get(tagkey, category=category))
def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -489,13 +490,14 @@ def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
session = accessed_obj.session
except AttributeError:
session = account.sessions.get()[0] # note-this doesn't work well
# for high multisession mode. We may need
# to change to sessiondb to resolve this
# for high multisession mode. We may need
# to change to sessiondb to resolve this
try:
return not account.get_puppet(session)
except TypeError:
return not session.get_puppet()
def objtag(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:

View file

@ -528,10 +528,10 @@ def search_prototype(
"""
# This will load the prototypes the first time they are searched
loaded = getattr(load_module_prototypes, '_LOADED', False)
loaded = getattr(load_module_prototypes, "_LOADED", False)
if not loaded:
load_module_prototypes()
setattr(load_module_prototypes, '_LOADED', True)
setattr(load_module_prototypes, "_LOADED", True)
# prototype keys are always in lowecase
if key:

View file

@ -2316,9 +2316,11 @@ def main():
if option in ("makemessages", "compilemessages"):
# some commands don't require the presence of a game directory to work
need_gamedir = False
if CURRENT_DIR != EVENNIA_LIB:
print("You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command.")
if CURRENT_DIR != EVENNIA_LIB:
print(
"You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command."
)
sys.exit()
if option in ("shell", "check", "makemigrations", "createsuperuser", "shell_plus"):

View file

@ -226,6 +226,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)
@ -236,6 +237,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)

View file

@ -423,6 +423,7 @@ class Evennia:
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
elif mode == "shutdown":
from evennia.objects.models import ObjectDB
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()

View file

@ -683,7 +683,7 @@ class ServerSessionHandler(SessionHandler):
Get a unique list of connected and logged-in Accounts.
Returns:
accounts (list): All conected Accounts (which may be fewer than the
accounts (list): All connected Accounts (which may be fewer than the
amount of Sessions due to multi-playing).
"""

View file

@ -197,7 +197,6 @@ class TestServer(TestCase):
class TestInitHooks(TestCase):
def setUp(self):
from evennia.utils import create

View file

@ -16,15 +16,18 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
avoid running the large number of tests defined by Django
"""
def setup_test_environment(self, **kwargs):
# the portal looping call starts before the unit-test suite so we
# can't mock it - instead we stop it before starting the test - otherwise
# we'd get unclean reactor errors across test boundaries.
from evennia.server.portal.portal import PORTAL
PORTAL.maintenance_task.stop()
# initialize evennia itself
import evennia
evennia._init()
from django.conf import settings
@ -37,6 +40,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
# remove testing flag after suite has run
from django.conf import settings
settings._TEST_ENVIRONMENT = False
super().teardown_test_environment(**kwargs)

View file

@ -218,6 +218,7 @@ class AttributeProperty:
"""
value = self._default
try:
<<<<<<< HEAD
value = self.at_get(getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
@ -225,6 +226,17 @@ class AttributeProperty:
strattr=self._strattr,
raise_exception=self._autocreate,
), instance)
=======
value = self.at_get(
getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
category=self._category,
strattr=self._strattr,
raise_exception=self._autocreate,
)
)
>>>>>>> ce3992f999a164881462d8f878d71a47a8f946cc
except AttributeError:
if self._autocreate:
# attribute didn't exist and autocreate is set

View file

@ -286,7 +286,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
categories = make_iter(category) if category else []
n_keys = len(keys)
n_categories = len(categories)
unique_categories = sorted(set(categories))
unique_categories = set(categories)
n_unique_categories = len(unique_categories)
dbmodel = self.model.__dbclass__.__name__.lower()

View file

@ -96,6 +96,7 @@ class Tag(models.Model):
# Handlers making use of the Tags model
#
class TagProperty:
"""
Tag property descriptor. Allows for setting tags on an object as Django-like 'fields'
@ -112,6 +113,7 @@ class TagProperty:
mytag2 = TagProperty(category="tagcategory")
"""
taghandler_name = "tags"
def __init__(self, category=None, data=None):
@ -134,10 +136,7 @@ class TagProperty:
"""
try:
return getattr(instance, self.taghandler_name).get(
key=self._key,
category=self._category,
return_list=False,
raise_exception=True
key=self._key, category=self._category, return_list=False, raise_exception=True
)
except AttributeError:
self.__set__(instance, self._category)
@ -150,9 +149,7 @@ class TagProperty:
self._category = category
(
getattr(instance, self.taghandler_name).add(
key=self._key,
category=self._category,
data=self._data
key=self._key, category=self._category, data=self._data
)
)
@ -430,8 +427,15 @@ class TagHandler(object):
return ret[0] if len(ret) == 1 else ret
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False,
raise_exception=False):
def get(
self,
key=None,
default=None,
category=None,
return_tagobj=False,
return_list=False,
raise_exception=False,
):
"""
Get the tag for the given key, category or combination of the two.
@ -613,6 +617,7 @@ class AliasProperty(TagProperty):
bob = AliasProperty()
"""
taghandler_name = "aliases"
@ -636,6 +641,7 @@ class PermissionProperty(TagProperty):
myperm = PermissionProperty()
"""
taghandler_name = "permissions"

View file

@ -142,6 +142,15 @@ class TestTypedObjectManager(BaseEvenniaTest):
[self.obj1],
)
def test_get_tag_with_any_including_nones(self):
self.obj1.tags.add("tagA", "categoryA")
self.assertEqual(
self._manager(
"get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"
),
[self.obj1],
)
def test_get_tag_withnomatch(self):
self.obj1.tags.add("tagC", "categoryC")
self.assertEqual(

View file

@ -12,6 +12,7 @@ evennia.OPTION_CLASSES
from pickle import dumps
from django.db.utils import OperationalError, ProgrammingError
from django.conf import settings
from evennia.utils.utils import class_from_module, callables_from_module
from evennia.utils import logger
@ -167,7 +168,6 @@ class GlobalScriptContainer(Container):
# store a hash representation of the setup
script.attributes.add("_global_script_settings", compare_hash, category="settings_hash")
script.start()
return script
@ -183,9 +183,16 @@ class GlobalScriptContainer(Container):
# populate self.typeclass_storage
self.load_data()
# start registered scripts
# make sure settings-defined scripts are loaded
for key in self.loaded_data:
self._load_script(key)
# start all global scripts
try:
for script in self._get_scripts():
script.start()
except (OperationalError, ProgrammingError):
# this can happen if db is not loaded yet (such as when building docs)
pass
def load_data(self):
"""

View file

@ -23,7 +23,7 @@ from collections import deque, OrderedDict, defaultdict
from collections.abc import MutableSequence, MutableSet, MutableMapping
try:
from pickle import dumps, loads
from pickle import dumps, loads, UnpicklingError
except ImportError:
from pickle import dumps, loads
from django.core.exceptions import ObjectDoesNotExist
@ -239,6 +239,9 @@ class _SaverMutable(object):
def __gt__(self, other):
return self._data > other
def __or__(self, other):
return self._data | other
@_save
def __setitem__(self, key, value):
self._data.__setitem__(key, self._convert_mutables(value))
@ -450,7 +453,9 @@ def deserialize(obj):
elif tname in ("_SaverOrderedDict", "OrderedDict"):
return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
elif tname in ("_SaverDefaultDict", "defaultdict"):
return defaultdict(obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()})
return defaultdict(
obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}
)
elif tname in _DESERIALIZE_MAPPING:
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
elif is_iter(obj):
@ -602,7 +607,9 @@ def to_pickle(data):
def process_item(item):
"""Recursive processor and identification of data"""
dtype = type(item)
if dtype in (str, int, float, bool, bytes, SafeString):
return item
elif dtype == tuple:
@ -612,7 +619,10 @@ def to_pickle(data):
elif dtype in (dict, _SaverDict):
return dict((process_item(key), process_item(val)) for key, val in item.items())
elif dtype in (defaultdict, _SaverDefaultDict):
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
return defaultdict(
item.default_factory,
((process_item(key), process_item(val)) for key, val in item.items()),
)
elif dtype in (set, _SaverSet):
return set(process_item(val) for val in item)
elif dtype in (OrderedDict, _SaverOrderedDict):
@ -620,7 +630,20 @@ def to_pickle(data):
elif dtype in (deque, _SaverDeque):
return deque(process_item(val) for val in item)
elif hasattr(item, "__iter__"):
# not one of the base types
if hasattr(item, "__serialize_dbobjs__"):
# Allows custom serialization of any dbobjects embedded in
# the item that Evennia will otherwise not find (these would
# otherwise lead to an error). Use the dbserialize helper from
# this method.
try:
item.__serialize_dbobjs__()
except TypeError as err:
# we catch typerrors so we can handle both classes (requiring
# classmethods) and instances
pass
if hasattr(item, "__iter__"):
# we try to conserve the iterable class, if not convert to list
try:
return item.__class__([process_item(val) for val in item])
@ -678,7 +701,10 @@ def from_pickle(data, db_obj=None):
elif dtype == dict:
return dict((process_item(key), process_item(val)) for key, val in item.items())
elif dtype == defaultdict:
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
return defaultdict(
item.default_factory,
((process_item(key), process_item(val)) for key, val in item.items()),
)
elif dtype == set:
return set(process_item(val) for val in item)
elif dtype == OrderedDict:
@ -692,6 +718,22 @@ def from_pickle(data, db_obj=None):
return item.__class__(process_item(val) for val in item)
except (AttributeError, TypeError):
return [process_item(val) for val in item]
if hasattr(item, "__deserialize_dbobjs__"):
# this allows the object to custom-deserialize any embedded dbobjs
# that we previously serialized with __serialize_dbobjs__.
# use the dbunserialize helper in this module.
try:
item.__deserialize_dbobjs__()
except (TypeError, UnpicklingError):
# handle recoveries both of classes (requiring classmethods
# or instances. Unpickling errors can happen when re-loading the
# data from cache (because the hidden entity was already
# deserialized and stored back on the object, unpickling it
# again fails). TODO: Maybe one could avoid this retry in a
# more graceful way?
pass
return item
def process_tree(item, parent):

View file

@ -274,12 +274,13 @@ import inspect
from ast import literal_eval
from fnmatch import fnmatch
from math import ceil
from inspect import isfunction, getargspec
from django.conf import settings
from evennia import Command, CmdSet
from evennia.utils import logger
from evennia.utils.evtable import EvTable
from evennia.utils.evtable import EvTable, EvColumn
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler
@ -1210,7 +1211,6 @@ class EvMenu:
Args:
optionlist (list): List of (key, description) tuples for every
option related to this node.
caller (Object, Account or None, optional): The caller of the node.
Returns:
options (str): The formatted option display.
@ -1229,7 +1229,7 @@ class EvMenu:
table = []
for key, desc in optionlist:
if key or desc:
desc_string = ": %s" % desc if desc else ""
desc_string = f": {desc}" if desc else ""
table_width_max = max(
table_width_max,
max(m_len(p) for p in key.split("\n"))
@ -1239,42 +1239,31 @@ class EvMenu:
raw_key = strip_ansi(key)
if raw_key != key:
# already decorations in key definition
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string))
table.append(f" |lc{raw_key}|lt{key}|le{desc_string}")
else:
# add a default white color to key
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string))
ncols = _MAX_TEXT_WIDTH // table_width_max # number of ncols
table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}")
ncols = _MAX_TEXT_WIDTH // table_width_max # number of columns
if ncols < 0:
# no visible option at all
# no visible options at all
return ""
ncols = ncols + 1 if ncols == 0 else ncols
# get the amount of rows needed (start with 4 rows)
nrows = 4
while nrows * ncols < nlist:
nrows += 1
ncols = nlist // nrows # number of full columns
nlastcol = nlist % nrows # number of elements in last column
ncols = 1 if ncols == 0 else ncols
# get the final column count
ncols = ncols + 1 if nlastcol > 0 else ncols
if ncols > 1:
# only extend if longer than one column
table.extend([" " for i in range(nrows - nlastcol)])
# minimum number of rows in a column
min_rows = 4
# build the actual table grid
table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)]
# split the items into columns
split = max(min_rows, ceil(len(table) / ncols))
max_end = len(table)
cols_list = []
for icol in range(ncols):
start = icol * split
end = min(start + split, max_end)
cols_list.append(EvColumn(*table[start:end]))
# adjust the width of each column
for icol in range(len(table)):
col_width = (
max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
)
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
# format the table into columns
return str(EvTable(table=table, border="none"))
return str(EvTable(table=cols_list, border="none"))
def node_formatter(self, nodetext, optionstext):
"""

View file

@ -181,7 +181,7 @@ class EvMore(object):
justify (bool, optional): If set, auto-justify long lines. This must be turned
off for fixed-width or formatted output, like tables. It's force-disabled
if `inp` is an EvTable.
justify_kwargs (dict, optional): Keywords for the justifiy function. Used only
justify_kwargs (dict, optional): Keywords for the justify function. Used only
if `justify` is True. If this is not set, default arguments will be used.
exit_on_lastpage (bool, optional): If reaching the last page without the
page being completely filled, exit pager immediately. If unset,
@ -507,7 +507,7 @@ class EvMore(object):
def page_formatter(self, page):
"""
Page formatter. Every page passes through this method. Override
it to customize behvaior per-page. A common use is to generate a new
it to customize behavior per-page. A common use is to generate a new
EvTable for every page (this is more efficient than to generate one huge
EvTable across many pages and feed it into EvMore all at once).

View file

@ -84,8 +84,8 @@ class _ParsedFunc:
# state storage
fullstr: str = ""
infuncstr: str = ""
single_quoted: bool = False
double_quoted: bool = False
single_quoted: int = -1
double_quoted: int = -1
current_kwarg: str = ""
open_lparens: int = 0
open_lsquate: int = 0
@ -318,8 +318,8 @@ class FuncParser:
# parsing state
callstack = []
single_quoted = False
double_quoted = False
single_quoted = -1
double_quoted = -1
open_lparens = 0 # open (
open_lsquare = 0 # open [
open_lcurly = 0 # open {
@ -330,6 +330,7 @@ class FuncParser:
curr_func = None
fullstr = "" # final string
infuncstr = "" # string parts inside the current level of $funcdef (including $)
literal_infuncstr = False
for char in string:
@ -373,12 +374,13 @@ class FuncParser:
curr_func.open_lcurly = open_lcurly
current_kwarg = ""
infuncstr = ""
single_quoted = False
double_quoted = False
single_quoted = -1
double_quoted = -1
open_lparens = 0
open_lsquare = 0
open_lcurly = 0
exec_return = ""
literal_infuncstr = False
callstack.append(curr_func)
# start a new func
@ -401,19 +403,41 @@ class FuncParser:
infuncstr += str(exec_return)
exec_return = ""
if char == "'": # note that this is the same as "\'"
if char == "'" and double_quoted < 0: # note that this is the same as "\'"
# a single quote - flip status
single_quoted = not single_quoted
infuncstr += char
if single_quoted == 0:
infuncstr = infuncstr[1:]
single_quoted = -1
elif single_quoted > 0:
prefix = infuncstr[0:single_quoted]
infuncstr = prefix + infuncstr[single_quoted + 1 :]
single_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
single_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if char == '"': # note that this is the same as '\"'
if char == '"' and single_quoted < 0: # note that this is the same as '\"'
# a double quote = flip status
double_quoted = not double_quoted
infuncstr += char
if double_quoted == 0:
infuncstr = infuncstr[1:]
double_quoted = -1
elif double_quoted > 0:
prefix = infuncstr[0:double_quoted]
infuncstr = prefix + infuncstr[double_quoted + 1 :]
double_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
double_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if double_quoted or single_quoted:
if double_quoted >= 0 or single_quoted >= 0:
# inside a string definition - this escapes everything else
infuncstr += char
continue
@ -477,12 +501,15 @@ class FuncParser:
else:
curr_func.args.append(exec_return)
else:
if not literal_infuncstr:
infuncstr = infuncstr.strip()
# store a string instead
if current_kwarg:
curr_func.kwargs[current_kwarg] = infuncstr.strip()
elif infuncstr.strip():
curr_func.kwargs[current_kwarg] = infuncstr
elif literal_infuncstr or infuncstr.strip():
# don't store the empty string
curr_func.args.append(infuncstr.strip())
curr_func.args.append(infuncstr)
# note that at this point either exec_return or infuncstr will
# be empty. We need to store the full string so we can print
@ -493,6 +520,7 @@ class FuncParser:
current_kwarg = ""
exec_return = ""
infuncstr = ""
literal_infuncstr = False
if char == ")":
# closing the function list - this means we have a
@ -536,6 +564,7 @@ class FuncParser:
if return_str:
exec_return = ""
infuncstr = ""
literal_infuncstr = False
continue
infuncstr += char

View file

@ -67,7 +67,7 @@ class TimeScript(DefaultScript):
callback(*args, **kwargs)
seconds = real_seconds_until(**self.db.gametime)
self.restart(interval=seconds)
self.start(interval=seconds, force_restart=True)
# Access functions

View file

@ -50,6 +50,7 @@ def _log(msg, logfunc, prefix="", **kwargs):
# log call functions (each has legacy aliases)
def log_info(msg, **kwargs):
"""
Logs any generic debugging/informative info that should appear in the log.
@ -62,6 +63,7 @@ def log_info(msg, **kwargs):
"""
_log(msg, log.info, **kwargs)
info = log_info
log_infomsg = log_info
log_msg = log_info
@ -79,6 +81,7 @@ def log_warn(msg, **kwargs):
"""
_log(msg, log.warn, **kwargs)
warn = log_warn
warning = log_warn
log_warnmsg = log_warn
@ -120,6 +123,7 @@ def log_trace(msg=None, **kwargs):
if msg:
_log(msg, log.error, prefix="!!", **kwargs)
log_tracemsg = log_trace
exception = log_trace
critical = log_trace
@ -156,6 +160,7 @@ def log_sec(msg, **kwargs):
"""
_log(msg, log.info, prefix="SS", **kwargs)
sec = log_sec
security = log_sec
log_secmsg = log_sec
@ -174,12 +179,12 @@ def log_server(msg, **kwargs):
_log(msg, log.info, prefix="Server", **kwargs)
class GetLogObserver:
"""
Sets up how the system logs are formatted.
"""
component_prefix = ""
event_levels = {
twisted_logger.LogLevel.debug: "??",
@ -207,8 +212,7 @@ class GetLogObserver:
event["log_format"] = str(event.get("log_format", ""))
component_prefix = self.component_prefix or ""
log_msg = twisted_logger.formatEventAsClassicLogText(
event,
formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
event, formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
)
return f"{component_prefix}{log_msg}"
@ -218,14 +222,15 @@ class GetLogObserver:
# Called by server/portal on startup
class GetPortalLogObserver(GetLogObserver):
component_prefix = "|Portal| "
class GetServerLogObserver(GetLogObserver):
component_prefix = ""
# logging overrides
@ -352,6 +357,7 @@ class WeeklyLogFile(logfile.DailyLogFile):
self.lastDate = max(self.lastDate, self.toDate())
self.size += len(data)
# Arbitrary file logger

View file

@ -96,7 +96,6 @@ DEFAULT_SETTING_RESETS = dict(
"evennia.game_template.server.conf.prototypefuncs",
],
BASE_GUEST_TYPECLASS="evennia.accounts.accounts.DefaultGuest",
# a special setting boolean _TEST_ENVIRONMENT is set by the test runner
# while the test suite is running.
)

View file

@ -15,9 +15,7 @@ class TestDbSerialize(TestCase):
"""
def setUp(self):
self.obj = DefaultObject(
db_key="Tester",
)
self.obj = DefaultObject(db_key="Tester")
self.obj.save()
def test_constants(self):
@ -62,10 +60,12 @@ class TestDbSerialize(TestCase):
self.obj.db.test.sort(key=lambda d: str(d))
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
def test_dict(self):
def test_saverdict(self):
self.obj.db.test = {"a": True}
self.obj.db.test.update({"b": False})
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
self.obj.db.test |= {"c": 5}
self.assertEqual(self.obj.db.test, {"a": True, "b": False, "c": 5})
@parameterized.expand(
[
@ -88,27 +88,88 @@ class TestDbSerialize(TestCase):
self.assertIsInstance(value, base_type)
self.assertNotIsInstance(value, saver_type)
self.assertEqual(value, default_value)
self.obj.db.test = {'a': True}
self.obj.db.test.update({'b': False})
self.assertEqual(self.obj.db.test, {'a': True, 'b': False})
self.obj.db.test = {"a": True}
self.obj.db.test.update({"b": False})
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
def test_defaultdict(self):
from collections import defaultdict
# baseline behavior for a defaultdict
_dd = defaultdict(list)
_dd['a']
self.assertEqual(_dd, {'a': []})
_dd["a"]
self.assertEqual(_dd, {"a": []})
# behavior after defaultdict is set as attribute
dd = defaultdict(list)
self.obj.db.test = dd
self.obj.db.test['a']
self.assertEqual(self.obj.db.test, {'a': []})
self.obj.db.test["a"]
self.assertEqual(self.obj.db.test, {"a": []})
self.obj.db.test['a'].append(1)
self.assertEqual(self.obj.db.test, {'a': [1]})
self.obj.db.test['a'].append(2)
self.assertEqual(self.obj.db.test, {'a': [1, 2]})
self.obj.db.test['a'].append(3)
self.assertEqual(self.obj.db.test, {'a': [1, 2, 3]})
self.obj.db.test["a"].append(1)
self.assertEqual(self.obj.db.test, {"a": [1]})
self.obj.db.test["a"].append(2)
self.assertEqual(self.obj.db.test, {"a": [1, 2]})
self.obj.db.test["a"].append(3)
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]})
self.obj.db.test |= {"b": [5, 6]}
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]})
class _InvalidContainer:
"""Container not saveable in Attribute (if obj is dbobj, it 'hides' it)"""
def __init__(self, obj):
self.hidden_obj = obj
class _ValidContainer(_InvalidContainer):
"""Container possible to save in Attribute (handles hidden dbobj explicitly)"""
def __serialize_dbobjs__(self):
self.hidden_obj = dbserialize.dbserialize(self.hidden_obj)
def __deserialize_dbobjs__(self):
self.hidden_obj = dbserialize.dbunserialize(self.hidden_obj)
class DbObjWrappers(TestCase):
"""
Test the `__serialize_dbobjs__` and `__deserialize_dbobjs__` methods.
"""
def setUp(self):
super().setUp()
self.dbobj1 = DefaultObject(db_key="Tester1")
self.dbobj1.save()
self.dbobj2 = DefaultObject(db_key="Tester2")
self.dbobj2.save()
def test_dbobj_hidden_obj__fail(self):
with self.assertRaises(TypeError):
self.dbobj1.db.testarg = _InvalidContainer(self.dbobj1)
def test_consecutive_fetch(self):
con = _ValidContainer(self.dbobj2)
self.dbobj1.db.testarg = con
attrobj = self.dbobj1.attributes.get("testarg", return_obj=True)
self.assertEqual(attrobj.value, con)
self.assertEqual(attrobj.value, con)
self.assertEqual(attrobj.value.hidden_obj, self.dbobj2)
def test_dbobj_hidden_obj__success(self):
con = _ValidContainer(self.dbobj2)
self.dbobj1.db.testarg = con
# accessing the same data twice
res1 = self.dbobj1.db.testarg
res2 = self.dbobj1.db.testarg
self.assertEqual(res1, res2)
self.assertEqual(res1, con)
self.assertEqual(res2, con)
self.assertEqual(res1.hidden_obj, self.dbobj2)
self.assertEqual(res2.hidden_obj, self.dbobj2)

View file

@ -44,6 +44,7 @@ def _double_callable(*args, **kwargs):
def _eval_callable(*args, **kwargs):
if args:
return simple_eval(args[0])
return ""
@ -113,25 +114,25 @@ class TestFuncParser(TestCase):
("$foo() Test noargs5", "_test() Test noargs5"),
("Test args1 $foo(a,b,c)", "Test args1 _test(a, b, c)"),
("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"),
("Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, ' too')"),
("Test args4 $foo('')", "Test args4 _test('')"),
('Test args4 $foo("")', 'Test args4 _test("")'),
(r"Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, too)"),
("Test args4 $foo('')", "Test args4 _test()"),
('Test args4 $foo("")', "Test args4 _test()"),
("Test args5 $foo(\(\))", "Test args5 _test(())"),
("Test args6 $foo(\()", "Test args6 _test(()"),
("Test args7 $foo(())", "Test args7 _test(())"),
("Test args8 $foo())", "Test args8 _test())"),
("Test args9 $foo(=)", "Test args9 _test(=)"),
("Test args10 $foo(\,)", "Test args10 _test(,)"),
("Test args10 $foo(',')", "Test args10 _test(',')"),
("Test args10 $foo(',')", "Test args10 _test(,)"),
("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax
(
"Test kwarg1 $bar(foo=1, bar='foo', too=ere)",
"Test kwarg1 _test(foo=1, bar='foo', too=ere)",
"Test kwarg1 _test(foo=1, bar=foo, too=ere)",
),
("Test kwarg2 $bar(foo,bar,too=ere)", "Test kwarg2 _test(foo, bar, too=ere)"),
("test kwarg3 $foo(foo = bar, bar = ere )", "test kwarg3 _test(foo=bar, bar=ere)"),
(
"test kwarg4 $foo(foo =' bar ',\" bar \"= ere )",
r"test kwarg4 $foo(foo =\' bar \',\" bar \"= ere )",
"test kwarg4 _test(foo=' bar ', \" bar \"=ere)",
),
(
@ -180,22 +181,29 @@ class TestFuncParser(TestCase):
("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"),
("Test eval1 $eval(21 + 21 - 10)", "Test eval1 32"),
("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"),
("Test eval3 $eval('21' + 'foo' + 'bar')", "Test eval3 21foobar"),
("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"),
("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"),
("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"),
("Test eval3 $eval(\"'21' + 'foo' + 'bar'\")", "Test eval3 21foobar"),
(r"Test eval4 $eval(\'21\' + \'$repl()\' + \"''\" + str(10 // 2))", "Test eval4 21rr5"),
(
r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))",
"Test eval5 21$repl()5",
),
("Test eval6 $eval(\"'$repl(a)' + '$repl(b)'\")", "Test eval6 rarrbr"),
("Test type1 $typ([1,2,3,4])", "Test type1 <class 'list'>"),
("Test type2 $typ((1,2,3,4))", "Test type2 <class 'tuple'>"),
("Test type3 $typ({1,2,3,4})", "Test type3 <class 'set'>"),
("Test type4 $typ({1:2,3:4})", "Test type4 <class 'dict'>"),
("Test type5 $typ(1), $typ(1.0)", "Test type5 <class 'int'>, <class 'float'>"),
("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 <class 'str'>, <class 'str'>"),
(
"Test type6 $typ(\"'1'\"), $typ('\"1.0\"')",
"Test type6 <class 'str'>, <class 'str'>",
),
("Test add1 $add(1, 2)", "Test add1 3"),
("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"),
("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"),
("Test literal2 $typ($lit(1))", "Test literal2 <class 'int'>"),
("Test literal3 $typ($lit(1)aaa)", "Test literal3 <class 'str'>"),
("Test literal4 $typ(aaa$lit(1))", "Test literal4 <class 'str'>"),
("Test spider's thread", "Test spider's thread"),
]
)
def test_parse(self, string, expected):
@ -258,7 +266,11 @@ class TestFuncParser(TestCase):
self.assertEqual([1, 2, 3, 4], ret)
self.assertTrue(isinstance(ret, list))
ret = self.parser.parse_to_any("$lit('')")
ret = self.parser.parse_to_any("$lit(\"''\")")
self.assertEqual("", ret)
self.assertTrue(isinstance(ret, str))
ret = self.parser.parse_to_any(r"$lit(\'\')")
self.assertEqual("", ret)
self.assertTrue(isinstance(ret, str))
@ -390,7 +402,8 @@ class TestDefaultCallables(TestCase):
("Some $rjust(Hello, 30)", "Some Hello"),
("Some $rjust(Hello, width=30)", "Some Hello"),
("Some $cjust(Hello, 30)", "Some Hello "),
("Some $eval('-'*20)Hello", "Some --------------------Hello"),
("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"),
('$crop("spider\'s silk", 5)', "spide"),
]
)
def test_other_callables(self, string, expected):
@ -455,15 +468,16 @@ class TestDefaultCallables(TestCase):
self.parser.parse(
"this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"
),
"this should be '''escaped,''' and '''instead,''' cropped with text. ",
"this should be escaped, and instead, cropped with text. ",
)
def test_escaped2(self):
raw_str = 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
expected = "this should be escaped, and instead, cropped with text. "
result = self.parser.parse(raw_str)
self.assertEqual(
self.parser.parse(
'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
),
'this should be """escaped,""" and """instead,""" cropped with text. ',
result,
expected,
)

View file

@ -0,0 +1,50 @@
from evennia.scripts.scripts import DefaultScript
from evennia.utils.test_resources import EvenniaTest
from evennia.utils.search import search_script_attribute, search_script_tag
class TestSearch(EvenniaTest):
def test_search_script_tag(self):
"""Check that a script can be found by its tag."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag")
found = search_script_tag("a-tag")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_tag_category(self):
"""Check that a script can be found by its tag and category."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("a-tag", category="a-category")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_tag_wrong_category(self):
"""Check that a script cannot be found by the wrong category."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("a-tag", category="wrong-category")
self.assertEqual(len(found), 0, errors)
def test_search_script_tag_wrong(self):
"""Check that a script cannot be found by the wrong tag."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("wrong-tag", category="a-category")
self.assertEqual(len(found), 0, errors)
def test_search_script_attribute(self):
"""Check that a script can be found by its attributes."""
script, errors = DefaultScript.create("a-script")
script.db.an_attribute = "some value"
found = search_script_attribute(key="an_attribute", value="some value")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_attribute_wrong(self):
"""Check that a script cannot be found by wrong value of its attributes."""
script, errors = DefaultScript.create("a-script")
script.db.an_attribute = "some value"
found = search_script_attribute(key="an_attribute", value="wrong value")
self.assertEqual(len(found), 0, errors)

View file

@ -7,20 +7,22 @@ import mock
class TestText2Html(TestCase):
def test_re_color(self):
def test_format_styles(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_color("foo"))
self.assertEqual("foo", parser.format_styles("foo"))
self.assertEqual(
'<span class="color-001">red</span>foo',
parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
parser.format_styles(
ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"
),
)
self.assertEqual(
'<span class="bgcolor-001">red</span>foo',
parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'<span class="bgcolor-001"><span class="color-002">red</span></span>foo',
parser.re_color(
'<span class="bgcolor-001 color-002">red</span>foo',
parser.format_styles(
ansi.ANSI_BACK_RED
+ ansi.ANSI_UNHILITE
+ ansi.ANSI_GREEN
@ -29,75 +31,25 @@ class TestText2Html(TestCase):
+ "foo"
),
)
@unittest.skip("parser issues")
def test_re_bold(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_bold("foo"))
self.assertEqual(
# "a <strong>red</strong>foo", # TODO: why not?
"a <strong>redfoo</strong>",
parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"),
'a <span class="underline">red</span>foo',
parser.format_styles("a " + ansi.ANSI_UNDERLINE + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a <span class="blink">red</span>foo',
parser.format_styles("a " + ansi.ANSI_BLINK + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a <span class="bgcolor-007 color-000">red</span>foo',
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
)
@unittest.skip("parser issues")
def test_re_underline(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_underline("foo"))
self.assertEqual(
'a <span class="underline">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_underline(
"a "
+ ansi.ANSI_UNDERLINE
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
)
@unittest.skip("parser issues")
def test_re_blinking(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_blinking("foo"))
self.assertEqual(
'a <span class="blink">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_blinking(
"a "
+ ansi.ANSI_BLINK
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
)
@unittest.skip("parser issues")
def test_re_inversing(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_inversing("foo"))
self.assertEqual(
'a <span class="inverse">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_inversing(
"a "
+ ansi.ANSI_INVERSE
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
)
@unittest.skip("parser issues")
def test_remove_bells(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.remove_bells("foo"))
self.assertEqual(
"a red" + ansi.ANSI_NORMAL + "foo",
parser.remove_bells(
"a "
+ ansi.ANSI_BEEP
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
parser.remove_bells("a " + ansi.ANSI_BEEP + "red" + ansi.ANSI_NORMAL + "foo"),
)
def test_remove_backspaces(self):
@ -110,7 +62,6 @@ class TestText2Html(TestCase):
self.assertEqual("foo", parser.convert_linebreaks("foo"))
self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n"))
@unittest.skip("parser issues")
def test_convert_urls(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.convert_urls("foo"))
@ -118,7 +69,6 @@ class TestText2Html(TestCase):
'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs',
parser.convert_urls("a http://redfoo runs"),
)
# TODO: doesn't URL encode correctly
def test_sub_mxp_links(self):
parser = text2html.HTML_PARSER
@ -186,22 +136,22 @@ class TestText2Html(TestCase):
self.assertEqual("foo", text2html.parse_html("foo"))
self.maxDiff = None
self.assertEqual(
# TODO: note that the blink is currently *not* correctly aborted
# with |n here! This is probably not possible to correctly handle
# with regex - a stateful parser may be needed.
# blink back-cyan normal underline red green yellow blue magenta cyan back-green
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
'<span class="blink">'
'<span class="bgcolor-006">Hello</span>' # noqa
'<span class="underline">'
'<span class="color-009">W</span>' # noqa
'<span class="color-010">o</span>'
'<span class="color-011">r</span>'
'<span class="color-012">l</span>'
'<span class="color-013">d</span>'
'<span class="color-014">!'
'<span class="bgcolor-002">!</span>' # noqa
"</span>"
"</span>"
'<span class="blink bgcolor-006">'
"Hello"
'</span><span class="underline color-009">'
"W"
'</span><span class="underline color-010">'
"o"
'</span><span class="underline color-011">'
"r"
'</span><span class="underline color-012">'
"l"
'</span><span class="underline color-013">'
"d"
'</span><span class="underline color-014">'
"!"
'</span><span class="underline bgcolor-002 color-014">'
"!"
"</span>",
)

View file

@ -12,11 +12,10 @@ import re
from html import escape as html_escape
from .ansi import *
# All xterm256 RGB equivalents
XTERM256_FG = "\033[38;5;%sm"
XTERM256_BG = "\033[48;5;%sm"
XTERM256_FG = "\033[38;5;{}m"
XTERM256_BG = "\033[48;5;{}m"
class TextToHTMLparser(object):
@ -25,77 +24,65 @@ class TextToHTMLparser(object):
"""
tabstop = 4
# mapping html color name <-> ansi code.
hilite = ANSI_HILITE
unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent.
normal = ANSI_NORMAL # "
underline = ANSI_UNDERLINE
blink = ANSI_BLINK
inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent?
colorcodes = [
("color-000", unhilite + ANSI_BLACK), # pure black
("color-001", unhilite + ANSI_RED),
("color-002", unhilite + ANSI_GREEN),
("color-003", unhilite + ANSI_YELLOW),
("color-004", unhilite + ANSI_BLUE),
("color-005", unhilite + ANSI_MAGENTA),
("color-006", unhilite + ANSI_CYAN),
("color-007", unhilite + ANSI_WHITE), # light grey
("color-008", hilite + ANSI_BLACK), # dark grey
("color-009", hilite + ANSI_RED),
("color-010", hilite + ANSI_GREEN),
("color-011", hilite + ANSI_YELLOW),
("color-012", hilite + ANSI_BLUE),
("color-013", hilite + ANSI_MAGENTA),
("color-014", hilite + ANSI_CYAN),
("color-015", hilite + ANSI_WHITE), # pure white
] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)]
colorback = [
("bgcolor-000", ANSI_BACK_BLACK), # pure black
("bgcolor-001", ANSI_BACK_RED),
("bgcolor-002", ANSI_BACK_GREEN),
("bgcolor-003", ANSI_BACK_YELLOW),
("bgcolor-004", ANSI_BACK_BLUE),
("bgcolor-005", ANSI_BACK_MAGENTA),
("bgcolor-006", ANSI_BACK_CYAN),
("bgcolor-007", ANSI_BACK_WHITE), # light grey
("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey
("bgcolor-009", hilite + ANSI_BACK_RED),
("bgcolor-010", hilite + ANSI_BACK_GREEN),
("bgcolor-011", hilite + ANSI_BACK_YELLOW),
("bgcolor-012", hilite + ANSI_BACK_BLUE),
("bgcolor-013", hilite + ANSI_BACK_MAGENTA),
("bgcolor-014", hilite + ANSI_BACK_CYAN),
("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white
] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)]
style_codes = [
# non-color style markers
ANSI_NORMAL,
ANSI_UNDERLINE,
ANSI_HILITE,
ANSI_UNHILITE,
ANSI_INVERSE,
ANSI_BLINK,
ANSI_INV_HILITE,
ANSI_BLINK_HILITE,
ANSI_INV_BLINK,
ANSI_INV_BLINK_HILITE,
]
# make sure to escape [
# colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
# colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
fg_colormap = dict((code, clr) for clr, code in colorcodes)
bg_colormap = dict((code, clr) for clr, code in colorback)
ansi_color_codes = [
# Foreground colors
ANSI_BLACK,
ANSI_RED,
ANSI_GREEN,
ANSI_YELLOW,
ANSI_BLUE,
ANSI_MAGENTA,
ANSI_CYAN,
ANSI_WHITE,
]
# create stop markers
fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
bgfgstop = bgstop[:-2] + fgstop
xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)]
fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)"
bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)"
bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}"
ansi_bg_codes = [
# Background colors
ANSI_BACK_BLACK,
ANSI_BACK_RED,
ANSI_BACK_GREEN,
ANSI_BACK_YELLOW,
ANSI_BACK_BLUE,
ANSI_BACK_MAGENTA,
ANSI_BACK_CYAN,
ANSI_BACK_WHITE,
]
# extract color markers, tagging the start marker and the text marked
re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")")
re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")")
re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")")
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
re_style = re.compile(
r"({})".format(
"|".join(
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
).replace("[", r"\[")
)
)
colorlist = (
[ANSI_UNHILITE + code for code in ansi_color_codes]
+ [ANSI_HILITE + code for code in ansi_color_codes]
+ xterm_fg_codes
)
bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes
re_normal = re.compile(normal.replace("[", r"\["))
re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop))
re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop))
re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop))
re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop))
re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop))
re_string = re.compile(
r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
re.S | re.M | re.I,
@ -106,100 +93,6 @@ class TextToHTMLparser(object):
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
def _sub_bgfg(self, colormatch):
# print("colormatch.groups()", colormatch.groups())
bgcode, fgcode, text = colormatch.groups()
if not fgcode:
ret = r"""<span class="%s">%s</span>""" % (
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
text,
)
else:
ret = r"""<span class="%s"><span class="%s">%s</span></span>""" % (
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")),
text,
)
return ret
def _sub_fg(self, colormatch):
code, text = colormatch.groups()
return r"""<span class="%s">%s</span>""" % (self.fg_colormap.get(code, "err"), text)
def _sub_bg(self, colormatch):
code, text = colormatch.groups()
return r"""<span class="%s">%s</span>""" % (self.bg_colormap.get(code, "err"), text)
def re_color(self, text):
"""
Replace ansi colors with html color class names. Let the
client choose how it will display colors, if it wishes to.
Args:
text (str): the string with color to replace.
Returns:
text (str): Re-colored text.
"""
text = self.re_bgfg.sub(self._sub_bgfg, text)
text = self.re_fgs.sub(self._sub_fg, text)
text = self.re_bgs.sub(self._sub_bg, text)
text = self.re_normal.sub("", text)
return text
def re_bold(self, text):
"""
Clean out superfluous hilights rather than set <strong>to make
it match the look of telnet.
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
text = self.re_hilite.sub(r"<strong>\1</strong>", text)
return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css.
def re_underline(self, text):
"""
Replace ansi underline with html underline class name.
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_uline.sub(r'<span class="underline">\1</span>', text)
def re_blinking(self, text):
"""
Replace ansi blink with custom blink css class
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_blink.sub(r'<span class="blink">\1</span>', text)
def re_inversing(self, text):
"""
Replace ansi inverse with custom inverse css class
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_inverse.sub(r'<span class="inverse">\1</span>', text)
def remove_bells(self, text):
"""
Remove ansi specials
@ -211,7 +104,7 @@ class TextToHTMLparser(object):
text (str): Processed text.
"""
return text.replace("\07", "")
return text.replace(ANSI_BEEP, "")
def remove_backspaces(self, text):
"""
@ -315,6 +208,128 @@ class TextToHTMLparser(object):
return text
return None
def format_styles(self, text):
"""
Takes a string with parsed ANSI codes and replaces them with
HTML spans and CSS classes.
Args:
text (str): The string to process.
Returns:
text (str): Processed text.
"""
# split out the ANSI codes and clean out any empty items
str_list = [substr for substr in self.re_style.split(text) if substr]
# initialize all the flags and classes
classes = []
clean = True
inverse = False
# default color is light grey - unhilite + white
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
# default bg is black
bg = ANSI_BACK_BLACK
for i, substr in enumerate(str_list):
# reset all current styling
if substr == ANSI_NORMAL:
# close any existing span if necessary
str_list[i] = "</span>" if not clean else ""
# reset to defaults
classes = []
clean = True
inverse = False
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
bg = ANSI_BACK_BLACK
# change color
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
# erase ANSI code from output
str_list[i] = ""
# set new color
fg = substr
# change bg color
elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
# erase ANSI code from output
str_list[i] = ""
# set new bg
bg = substr
# non-color codes
elif substr in self.style_codes:
# erase ANSI code from output
str_list[i] = ""
# hilight codes
if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
# set new hilight status
hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
# inversion codes
if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
inverse = True
# blink codes
if (
substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
and "blink" not in classes
):
classes.append("blink")
# underline
if substr == ANSI_UNDERLINE and "underline" not in classes:
classes.append("underline")
else:
# normal text, add text back to list
if not str_list[i - 1]:
# prior entry was cleared, which means style change
# get indices for the fg and bg codes
bg_index = self.bglist.index(bg)
try:
color_index = self.colorlist.index(hilight + fg)
except ValueError:
# xterm256 colors don't have the hilight codes
color_index = self.colorlist.index(fg)
if inverse:
# inverse means swap fg and bg indices
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
else:
# use fg and bg indices for classes
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
color_class = "color-{}".format(str(color_index).rjust(3, "0"))
# black bg is the default, don't explicitly style
if bg_class != "bgcolor-000":
classes.append(bg_class)
# light grey text is the default, don't explicitly style
if color_class != "color-007":
classes.append(color_class)
# define the new style span
prefix = '<span class="{}">'.format(" ".join(classes))
# close any prior span
if not clean:
prefix = "</span>" + prefix
# add span to output
str_list[i - 1] = prefix
# clean out color classes to easily update next time
classes = [cls for cls in classes if "color" not in cls]
# flag as currently being styled
clean = False
# close span if necessary
if not clean:
str_list.append("</span>")
# recombine back into string
return "".join(str_list)
def parse(self, text, strip_ansi=False):
"""
Main access function, converts a text containing ANSI codes
@ -328,19 +343,14 @@ class TextToHTMLparser(object):
text (str): Parsed text.
"""
# print(f"incoming text:\n{text}")
# parse everything to ansi first
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
# convert all ansi to html
result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
result = self.re_color(result)
result = self.re_bold(result)
result = self.re_underline(result)
result = self.re_blinking(result)
result = self.re_inversing(result)
result = self.remove_bells(result)
result = self.format_styles(result)
result = self.convert_linebreaks(result)
result = self.remove_backspaces(result)
result = self.convert_urls(result)

View file

@ -819,7 +819,7 @@ def latinify(string, default="?", pure_ascii=False):
This is used as a last resort when normal encoding does not work.
Arguments:
string (str): A string to convert to 'safe characters' convertable
string (str): A string to convert to 'safe characters' convertible
to an latin-1 bytestring later.
default (str, optional): Characters resisting mapping will be replaced
with this character or string. The intent is to apply an encode operation
@ -1078,7 +1078,7 @@ def delay(timedelay, callback, *args, **kwargs):
Keep in mind that persistent tasks arguments and callback should not
use memory references.
If persistent is set to True the delay function will return an int
which is the task's id itended for use with TASK_HANDLER's do_task
which is the task's id intended for use with TASK_HANDLER's do_task
and remove methods.
All persistent tasks whose time delays have passed will be called on server startup.
@ -1531,12 +1531,12 @@ def class_from_module(path, defaultpaths=None, fallback=None):
defaultpaths (iterable, optional): If a direct import from `path` fails,
try subsequent imports by prepending those paths to `path`.
fallback (str): If all other attempts fail, use this path as a fallback.
This is intended as a last-resport. In the example of Evennia
This is intended as a last-resort. In the example of Evennia
loading, this would be a path to a default parent class in the
evennia repo itself.
Returns:
class (Class): An uninstatiated class recovered from path.
class (Class): An uninstantiated class recovered from path.
Raises:
ImportError: If all loading failed.
@ -1675,7 +1675,7 @@ def string_partial_matching(alternatives, inp, ret_index=True):
Matching is made from the start of each subword in each
alternative. Case is not important. So e.g. "bi sh sw" or just
"big" or "shiny" or "sw" will match "Big shiny sword". Scoring is
done to allow to separate by most common demoninator. You will get
done to allow to separate by most common denominator. You will get
multiple matches returned if appropriate.
Args:
@ -1749,7 +1749,7 @@ def format_table(table, extra_space=1):
ftable = format_table([[1,2,3], [4,5,6]])
string = ""
for ir, row in enumarate(ftable):
for ir, row in enumerate(ftable):
if ir == 0:
# make first row white
string += "\\n|w" + "".join(row) + "|n"
@ -2695,6 +2695,7 @@ def copy_word_case(base_word, new_word):
+ excess
)
def run_in_main_thread(function_or_method, *args, **kwargs):
"""
Force a callable to execute in the main Evennia thread. This is only relevant when

View file

@ -149,7 +149,7 @@ An "emitter" object must have a function
// kwargs (obj): keyword-args to listener
//
emit: function (cmdname, args, kwargs) {
if (kwargs.cmdid) {
if (kwargs.cmdid && (kwargs.cmdid in cmdmap)) {
cmdmap[kwargs.cmdid].apply(this, [args, kwargs]);
delete cmdmap[kwargs.cmdid];
}

View file

@ -14,6 +14,7 @@ autobahn >= 20.7.1, < 21.0.0
lunr == 0.6.0
simpleeval <= 1.0
uritemplate == 4.1.1
Jinja2 < 3.1
# try to resolve dependency issue in py3.7
attrs >= 19.2.0