1# pyenchant
2#
3# Copyright (C) 2004-2011, Ryan Kelly
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18# Boston, MA 02111-1307, USA.
19#
20# In addition, as a special exception, you are
21# given permission to link the code of this program with
22# non-LGPL Spelling Provider libraries (eg: a MSFT Office
23# spell checker backend) and distribute linked combinations including
24# the two.  You must obey the GNU Lesser General Public License in all
25# respects for all of the code used other than said providers.  If you modify
26# this file, you may extend this exception to your version of the
27# file, but you are not obligated to do so.  If you do not wish to
28# do so, delete this exception statement from your version.
29#
30"""
31enchant:  Access to the enchant spellchecking library
32=====================================================
33
34This module provides several classes for performing spell checking
35via the Enchant spellchecking library.  For more details on Enchant,
36visit the project website:
37
38    https://abiword.github.io/enchant/
39
40Spellchecking is performed using 'Dict' objects, which represent
41a language dictionary.  Their use is best demonstrated by a quick
42example::
43
44    >>> import enchant
45    >>> d = enchant.Dict("en_US")   # create dictionary for US English
46    >>> d.check("enchant")
47    True
48    >>> d.check("enchnt")
49    False
50    >>> d.suggest("enchnt")
51    ['enchant', 'enchants', 'enchanter', 'penchant', 'incant', 'enchain', 'enchanted']
52
53Languages are identified by standard string tags such as "en" (English)
54and "fr" (French).  Specific language dialects can be specified by
55including an additional code - for example, "en_AU" refers to Australian
56English.  The later form is preferred as it is more widely supported.
57
58To check whether a dictionary exists for a given language, the function
59'dict_exists' is available.  Dictionaries may also be created using the
60function 'request_dict'.
61
62A finer degree of control over the dictionaries and how they are created
63can be obtained using one or more 'Broker' objects.  These objects are
64responsible for locating dictionaries for a specific language.
65
66Note that unicode strings are expected throughout the entire API.
67Bytestrings should not be passed into any function.
68
69Errors that occur in this module are reported by raising subclasses
70of 'Error'.
71
72"""
73_DOC_ERRORS = ["enchnt", "enchnt", "incant", "fr"]
74
75__version__ = "3.2.2"
76
77import os
78import warnings
79
80try:
81    from enchant import _enchant as _e
82except ImportError:
83    if not os.environ.get("PYENCHANT_IGNORE_MISSING_LIB", False):
84        raise
85    _e = None
86
87from enchant.errors import Error, DictNotFoundError
88from enchant.utils import get_default_language
89from enchant.pypwl import PyPWL
90
91
92class ProviderDesc:
93    """Simple class describing an Enchant provider.
94
95    Each provider has the following information associated with it:
96
97        * name:        Internal provider name (e.g. "aspell")
98        * desc:        Human-readable description (e.g. "Aspell Provider")
99        * file:        Location of the library containing the provider
100
101    """
102
103    _DOC_ERRORS = ["desc"]
104
105    def __init__(self, name, desc, file):
106        self.name = name
107        self.desc = desc
108        self.file = file
109
110    def __str__(self):
111        return "<Enchant: %s>" % self.desc
112
113    def __repr__(self):
114        return str(self)
115
116    def __eq__(self, pd):
117        """Equality operator on ProviderDesc objects."""
118        return self.name == pd.name and self.desc == pd.desc and self.file == pd.file
119
120    def __hash__(self):
121        """Hash operator on ProviderDesc objects."""
122        return hash(self.name + self.desc + self.file)
123
124
125class _EnchantObject:
126    """Base class for enchant objects.
127
128    This class implements some general functionality for interfacing with
129    the '_enchant' C-library in a consistent way.  All public objects
130    from the 'enchant' module are subclasses of this class.
131
132    All enchant objects have an attribute '_this' which contains the
133    pointer to the underlying C-library object.  The method '_check_this'
134    can be called to ensure that this point is not None, raising an
135    exception if it is.
136    """
137
138    def __init__(self):
139        """_EnchantObject constructor."""
140        self._this = None
141        #  To be importable when enchant C lib is missing, we need
142        #  to create a dummy default broker.
143        if _e is not None:
144            self._init_this()
145
146    def _check_this(self, msg=None):
147        """Check that self._this is set to a pointer, rather than None."""
148        if self._this is None:
149            if msg is None:
150                msg = "%s unusable: the underlying C-library object has been freed."
151                msg = msg % (self.__class__.__name__,)
152            raise Error(msg)
153
154    def _init_this(self):
155        """Initialise the underlying C-library object pointer."""
156        raise NotImplementedError
157
158    def _raise_error(self, default="Unspecified Error", eclass=Error):
159        """Raise an exception based on available error messages.
160
161        This method causes an Error to be raised.  Subclasses should
162        override it to retrieve an error indication from the underlying
163        API if possible.  If such a message cannot be retrieved, the
164        argument value <default> is used.  The class of the exception
165        can be specified using the argument <eclass>
166        """
167        raise eclass(default)
168
169    _raise_error._DOC_ERRORS = ["eclass"]
170
171    def __getstate__(self):
172        """Customize pickling of PyEnchant objects.
173
174        Since it's not safe for multiple objects to share the same C-library
175        object, we make sure it's unset when pickling.
176        """
177        state = self.__dict__.copy()
178        state["_this"] = None
179        return state
180
181    def __setstate__(self, state):
182        self.__dict__.update(state)
183        self._init_this()
184
185
186class Broker(_EnchantObject):
187    """Broker object for the Enchant spellchecker.
188
189    Broker objects are responsible for locating and managing dictionaries.
190    Unless custom functionality is required, there is no need to use Broker
191    objects directly. The 'enchant' module provides a default broker object
192    so that 'Dict' objects can be created directly.
193
194    The most important methods of this class include:
195
196        * :py:meth:`dict_exists`:   check existence of a specific language dictionary
197        * :py:meth:`request_dict`:  obtain a dictionary for specific language
198        * :py:meth:`set_ordering`:  specify which dictionaries to try for a given language.
199
200    """
201
202    def __init__(self):
203        """Broker object constructor.
204
205        This method is the constructor for the 'Broker' object.  No
206        arguments are required.
207        """
208        super().__init__()
209
210    def _init_this(self):
211        self._this = _e.broker_init()
212        if not self._this:
213            raise Error("Could not initialise an enchant broker.")
214        self._live_dicts = {}
215
216    def __del__(self):
217        """Broker object destructor."""
218        # Calling free() might fail if python is shutting down
219        try:
220            self._free()
221        except (AttributeError, TypeError):
222            pass
223
224    def __getstate__(self):
225        state = super().__getstate__()
226        state.pop("_live_dicts")
227        return state
228
229    def _raise_error(self, default="Unspecified Error", eclass=Error):
230        """Overrides _EnchantObject._raise_error to check broker errors."""
231        err = _e.broker_get_error(self._this)
232        if err == "" or err is None:
233            raise eclass(default)
234        raise eclass(err.decode())
235
236    def _free(self):
237        """Free system resource associated with a Broker object.
238
239        This method can be called to free the underlying system resources
240        associated with a Broker object.  It is called automatically when
241        the object is garbage collected.  If called explicitly, the
242        Broker and any associated Dict objects must no longer be used.
243        """
244        if self._this is not None:
245            # During shutdown, this finalizer may be called before
246            # some Dict finalizers.  Ensure all pointers are freed.
247            for (dict, count) in list(self._live_dicts.items()):
248                while count:
249                    self._free_dict_data(dict)
250                    count -= 1
251            _e.broker_free(self._this)
252            self._this = None
253
254    def request_dict(self, tag=None):
255        """Request a Dict object for the language specified by <tag>.
256
257        This method constructs and returns a Dict object for the
258        requested language.  'tag' should be a string of the appropriate
259        form for specifying a language, such as "fr" (French) or "en_AU"
260        (Australian English).  The existence of a specific language can
261        be tested using the 'dict_exists' method.
262
263        If <tag> is not given or is None, an attempt is made to determine
264        the current language in use.  If this cannot be determined, Error
265        is raised.
266
267        .. note::
268            this method is functionally equivalent to calling the Dict()
269            constructor and passing in the <broker> argument.
270
271        """
272        return Dict(tag, self)
273
274    request_dict._DOC_ERRORS = ["fr"]
275
276    def _request_dict_data(self, tag):
277        """Request raw C pointer data for a dictionary.
278
279        This method call passes on the call to the C library, and does
280        some internal bookkeeping.
281        """
282        self._check_this()
283        new_dict = _e.broker_request_dict(self._this, tag.encode())
284        if new_dict is None:
285            e_str = "Dictionary for language '%s' could not be found\n"
286            e_str += "Please check https://pyenchant.github.io/pyenchant/ for details"
287            self._raise_error(e_str % (tag,), DictNotFoundError)
288        if new_dict not in self._live_dicts:
289            self._live_dicts[new_dict] = 1
290        else:
291            self._live_dicts[new_dict] += 1
292        return new_dict
293
294    def request_pwl_dict(self, pwl):
295        """Request a Dict object for a personal word list.
296
297        This method behaves as 'request_dict' but rather than returning
298        a dictionary for a specific language, it returns a dictionary
299        referencing a personal word list.  A personal word list is a file
300        of custom dictionary entries, one word per line.
301        """
302        self._check_this()
303        new_dict = _e.broker_request_pwl_dict(self._this, pwl.encode())
304        if new_dict is None:
305            e_str = "Personal Word List file '%s' could not be loaded"
306            self._raise_error(e_str % (pwl,))
307        if new_dict not in self._live_dicts:
308            self._live_dicts[new_dict] = 1
309        else:
310            self._live_dicts[new_dict] += 1
311        d = Dict(False)
312        d._switch_this(new_dict, self)
313        return d
314
315    def _free_dict(self, dict):
316        """Free memory associated with a dictionary.
317
318        This method frees system resources associated with a Dict object.
319        It is equivalent to calling the object's 'free' method.  Once this
320        method has been called on a dictionary, it must not be used again.
321        """
322        self._free_dict_data(dict._this)
323        dict._this = None
324        dict._broker = None
325
326    def _free_dict_data(self, dict):
327        """Free the underlying pointer for a dict."""
328        self._check_this()
329        _e.broker_free_dict(self._this, dict)
330        self._live_dicts[dict] -= 1
331        if self._live_dicts[dict] == 0:
332            del self._live_dicts[dict]
333
334    def dict_exists(self, tag):
335        """Check availability of a dictionary.
336
337        This method checks whether there is a dictionary available for
338        the language specified by 'tag'.  It returns True if a dictionary
339        is available, and False otherwise.
340        """
341        self._check_this()
342        val = _e.broker_dict_exists(self._this, tag.encode())
343        return bool(val)
344
345    def set_ordering(self, tag, ordering):
346        """Set dictionary preferences for a language.
347
348        The Enchant library supports the use of multiple dictionary programs
349        and multiple languages.  This method specifies which dictionaries
350        the broker should prefer when dealing with a given language.  'tag'
351        must be an appropriate language specification and 'ordering' is a
352        string listing the dictionaries in order of preference.  For example
353        a valid ordering might be "aspell,myspell,ispell".
354        The value of 'tag' can also be set to "*" to set a default ordering
355        for all languages for which one has not been set explicitly.
356        """
357        self._check_this()
358        _e.broker_set_ordering(self._this, tag.encode(), ordering.encode())
359
360    def describe(self):
361        """Return list of provider descriptions.
362
363        This method returns a list of descriptions of each of the
364        dictionary providers available.  Each entry in the list is a
365        ProviderDesc object.
366        """
367        self._check_this()
368        self.__describe_result = []
369        _e.broker_describe(self._this, self.__describe_callback)
370        return [ProviderDesc(*r) for r in self.__describe_result]
371
372    def __describe_callback(self, name, desc, file):
373        """Collector callback for dictionary description.
374
375        This method is used as a callback into the _enchant function
376        'enchant_broker_describe'.  It collects the given arguments in
377        a tuple and appends them to the list '__describe_result'.
378        """
379        name = name.decode()
380        desc = desc.decode()
381        file = file.decode()
382        self.__describe_result.append((name, desc, file))
383
384    def list_dicts(self):
385        """Return list of available dictionaries.
386
387        This method returns a list of dictionaries available to the
388        broker.  Each entry in the list is a two-tuple of the form:
389
390            (tag,provider)
391
392        where <tag> is the language lag for the dictionary and
393        <provider> is a ProviderDesc object describing the provider
394        through which that dictionary can be obtained.
395        """
396        self._check_this()
397        self.__list_dicts_result = []
398        _e.broker_list_dicts(self._this, self.__list_dicts_callback)
399        return [(r[0], ProviderDesc(*r[1])) for r in self.__list_dicts_result]
400
401    def __list_dicts_callback(self, tag, name, desc, file):
402        """Collector callback for listing dictionaries.
403
404        This method is used as a callback into the _enchant function
405        'enchant_broker_list_dicts'.  It collects the given arguments into
406        an appropriate tuple and appends them to '__list_dicts_result'.
407        """
408        tag = tag.decode()
409        name = name.decode()
410        desc = desc.decode()
411        file = file.decode()
412        self.__list_dicts_result.append((tag, (name, desc, file)))
413
414    def list_languages(self):
415        """List languages for which dictionaries are available.
416
417        This function returns a list of language tags for which a
418        dictionary is available.
419        """
420        langs = []
421        for (tag, prov) in self.list_dicts():
422            if tag not in langs:
423                langs.append(tag)
424        return langs
425
426    def __describe_dict(self, dict_data):
427        """Get the description tuple for a dict data object.
428        <dict_data> must be a C-library pointer to an enchant dictionary.
429        The return value is a tuple of the form:
430                (<tag>,<name>,<desc>,<file>)
431        """
432        # Define local callback function
433        cb_result = []
434
435        def cb_func(tag, name, desc, file):
436            tag = tag.decode()
437            name = name.decode()
438            desc = desc.decode()
439            file = file.decode()
440            cb_result.append((tag, name, desc, file))
441
442        # Actually call the describer function
443        _e.dict_describe(dict_data, cb_func)
444        return cb_result[0]
445
446    __describe_dict._DOC_ERRORS = ["desc"]
447
448    def get_param(self, name):
449        """Get the value of a named parameter on this broker.
450
451        Parameters are used to provide runtime information to individual
452        provider backends.  See the method :py:meth:`set_param` for more details.
453
454        .. warning::
455
456            This method does **not** work when using the Enchant C
457            library version 2.0 and above
458        """
459        param = _e.broker_get_param(self._this, name.encode())
460        if param is not None:
461            param = param.decode()
462        return param
463
464    get_param._DOC_ERRORS = ["param"]
465
466    def set_param(self, name, value):
467        """Set the value of a named parameter on this broker.
468
469        Parameters are used to provide runtime information to individual
470        provider backends.
471
472        .. warning::
473
474            This method does **not** work when using the Enchant C
475            library version 2.0 and above
476        """
477        name = name.encode()
478        if value is not None:
479            value = value.encode()
480        _e.broker_set_param(self._this, name, value)
481
482
483class Dict(_EnchantObject):
484    """Dictionary object for the Enchant spellchecker.
485
486    Dictionary objects are responsible for checking the spelling of words
487    and suggesting possible corrections.  Each dictionary is owned by a
488    Broker object, but unless a new Broker has explicitly been created
489    then this will be the 'enchant' module default Broker and is of little
490    interest.
491
492    The important methods of this class include:
493
494        * check():              check whether a word id spelled correctly
495        * suggest():            suggest correct spellings for a word
496        * add():                add a word to the user's personal dictionary
497        * remove():             add a word to the user's personal exclude list
498        * add_to_session():     add a word to the current spellcheck session
499        * store_replacement():  indicate a replacement for a given word
500
501    Information about the dictionary is available using the following
502    attributes:
503
504        * tag:        the language tag of the dictionary
505        * provider:   a ProviderDesc object for the dictionary provider
506
507    """
508
509    def __init__(self, tag=None, broker=None):
510        """Dict object constructor.
511
512        A dictionary belongs to a specific language, identified by the
513        string <tag>.  If the tag is not given or is None, an attempt to
514        determine the language currently in use is made using the 'locale'
515        module.  If the current language cannot be determined, Error is raised.
516
517        If <tag> is instead given the value of False, a 'dead' Dict object
518        is created without any reference to a language.  This is typically
519        only useful within PyEnchant itself.  Any other non-string value
520        for <tag> raises Error.
521
522        Each dictionary must also have an associated Broker object which
523        obtains the dictionary information from the underlying system. This
524        may be specified using <broker>.  If not given, the default broker
525        is used.
526        """
527        # Initialise misc object attributes to None
528        self.provider = None
529        # If no tag was given, use the default language
530        if tag is None:
531            tag = get_default_language()
532            if tag is None:
533                err = "No tag specified and default language could not "
534                err = err + "be determined."
535                raise Error(err)
536        self.tag = tag
537        # If no broker was given, use the default broker
538        if broker is None:
539            broker = _broker
540        self._broker = broker
541        # Now let the superclass initialise the C-library object
542        super().__init__()
543
544    def _init_this(self):
545        # Create dead object if False was given as the tag.
546        # Otherwise, use the broker to get C-library pointer data.
547        self._this = None
548        if self.tag:
549            this = self._broker._request_dict_data(self.tag)
550            self._switch_this(this, self._broker)
551
552    def __del__(self):
553        """Dict object destructor."""
554        # Calling free() might fail if python is shutting down
555        try:
556            self._free()
557        except (AttributeError, TypeError):
558            pass
559
560    def _switch_this(self, this, broker):
561        """Switch the underlying C-library pointer for this object.
562
563        As all useful state for a Dict is stored by the underlying C-library
564        pointer, it is very convenient to allow this to be switched at
565        run-time.  Pass a new dict data object into this method to affect
566        the necessary changes.  The creating Broker object (at the Python
567        level) must also be provided.
568
569        This should *never* *ever* be used by application code.  It's
570        a convenience for developers only, replacing the clunkier <data>
571        parameter to __init__ from earlier versions.
572        """
573        # Free old dict data
574        Dict._free(self)
575        # Hook in the new stuff
576        self._this = this
577        self._broker = broker
578        # Update object properties
579        desc = self.__describe(check_this=False)
580        self.tag = desc[0]
581        self.provider = ProviderDesc(*desc[1:])
582
583    _switch_this._DOC_ERRORS = ["init"]
584
585    def _check_this(self, msg=None):
586        """Extend _EnchantObject._check_this() to check Broker validity.
587
588        It is possible for the managing Broker object to be freed without
589        freeing the Dict.  Thus validity checking must take into account
590        self._broker._this as well as self._this.
591        """
592        if self._broker is None or self._broker._this is None:
593            self._this = None
594        super()._check_this(msg)
595
596    def _raise_error(self, default="Unspecified Error", eclass=Error):
597        """Overrides _EnchantObject._raise_error to check dict errors."""
598        err = _e.dict_get_error(self._this)
599        if err == "" or err is None:
600            raise eclass(default)
601        raise eclass(err.decode())
602
603    def _free(self):
604        """Free the system resources associated with a Dict object.
605
606        This method frees underlying system resources for a Dict object.
607        Once it has been called, the Dict object must no longer be used.
608        It is called automatically when the object is garbage collected.
609        """
610        if self._this is not None:
611            # The broker may have been freed before the dict.
612            # It will have freed the underlying pointers already.
613            if self._broker is not None and self._broker._this is not None:
614                self._broker._free_dict(self)
615
616    def check(self, word):
617        """Check spelling of a word.
618
619        This method takes a word in the dictionary language and returns
620        True if it is correctly spelled, and false otherwise.
621        """
622        self._check_this()
623        # Enchant asserts that the word is non-empty.
624        # Check it up-front to avoid nasty warnings on stderr.
625        if len(word) == 0:
626            raise ValueError("can't check spelling of empty string")
627        val = _e.dict_check(self._this, word.encode())
628        if val == 0:
629            return True
630        if val > 0:
631            return False
632        self._raise_error()
633
634    def suggest(self, word):
635        """Suggest possible spellings for a word.
636
637        This method tries to guess the correct spelling for a given
638        word, returning the possibilities in a list.
639        """
640        self._check_this()
641        # Enchant asserts that the word is non-empty.
642        # Check it up-front to avoid nasty warnings on stderr.
643        if len(word) == 0:
644            raise ValueError("can't suggest spellings for empty string")
645        suggs = _e.dict_suggest(self._this, word.encode())
646        return [w.decode() for w in suggs]
647
648    def add(self, word):
649        """Add a word to the user's personal word list."""
650        self._check_this()
651        _e.dict_add(self._this, word.encode())
652
653    def remove(self, word):
654        """Add a word to the user's personal exclude list."""
655        self._check_this()
656        _e.dict_remove(self._this, word.encode())
657
658    def add_to_pwl(self, word):
659        """Add a word to the user's personal word list."""
660        warnings.warn(
661            "Dict.add_to_pwl is deprecated, please use Dict.add",
662            category=DeprecationWarning,
663            stacklevel=2,
664        )
665        self._check_this()
666        _e.dict_add_to_pwl(self._this, word.encode())
667
668    def add_to_session(self, word):
669        """Add a word to the session personal list."""
670        self._check_this()
671        _e.dict_add_to_session(self._this, word.encode())
672
673    def remove_from_session(self, word):
674        """Add a word to the session exclude list."""
675        self._check_this()
676        _e.dict_remove_from_session(self._this, word.encode())
677
678    def is_added(self, word):
679        """Check whether a word is in the personal word list."""
680        self._check_this()
681        return _e.dict_is_added(self._this, word.encode())
682
683    def is_removed(self, word):
684        """Check whether a word is in the personal exclude list."""
685        self._check_this()
686        return _e.dict_is_removed(self._this, word.encode())
687
688    def store_replacement(self, mis, cor):
689        """Store a replacement spelling for a miss-spelled word.
690
691        This method makes a suggestion to the spellchecking engine that the
692        miss-spelled word <mis> is in fact correctly spelled as <cor>.  Such
693        a suggestion will typically mean that <cor> appears early in the
694        list of suggested spellings offered for later instances of <mis>.
695        """
696        if not mis:
697            raise ValueError("can't store replacement for an empty string")
698        if not cor:
699            raise ValueError("can't store empty string as a replacement")
700        self._check_this()
701        _e.dict_store_replacement(self._this, mis.encode(), cor.encode())
702
703    store_replacement._DOC_ERRORS = ["mis", "mis"]
704
705    def __describe(self, check_this=True):
706        """Return a tuple describing the dictionary.
707
708        This method returns a four-element tuple describing the underlying
709        spellchecker system providing the dictionary.  It will contain the
710        following strings:
711
712            * language tag
713            * name of dictionary provider
714            * description of dictionary provider
715            * dictionary file
716
717        Direct use of this method is not recommended - instead, access this
718        information through the 'tag' and 'provider' attributes.
719        """
720        if check_this:
721            self._check_this()
722        _e.dict_describe(self._this, self.__describe_callback)
723        return self.__describe_result
724
725    def __describe_callback(self, tag, name, desc, file):
726        """Collector callback for dictionary description.
727
728        This method is used as a callback into the _enchant function
729        'enchant_dict_describe'.  It collects the given arguments in
730        a tuple and stores them in the attribute '__describe_result'.
731        """
732        tag = tag.decode()
733        name = name.decode()
734        desc = desc.decode()
735        file = file.decode()
736        self.__describe_result = (tag, name, desc, file)
737
738
739class DictWithPWL(Dict):
740    """Dictionary with separately-managed personal word list.
741
742    .. note::
743        As of version 1.4.0, enchant manages a per-user pwl and
744        exclude list.  This class is now only needed if you want
745        to explicitly maintain a separate word list in addition to
746        the default one.
747
748    This class behaves as the standard Dict class, but also manages a
749    personal word list stored in a separate file.  The file must be
750    specified at creation time by the 'pwl' argument to the constructor.
751    Words added to the dictionary are automatically appended to the pwl file.
752
753    A personal exclude list can also be managed, by passing another filename
754    to the constructor in the optional 'pel' argument.  If this is not given,
755    requests to exclude words are ignored.
756
757    If either 'pwl' or 'pel' are None, an in-memory word list is used.
758    This will prevent calls to add() and remove() from affecting the user's
759    default word lists.
760
761    The Dict object managing the PWL is available as the 'pwl' attribute.
762    The Dict object managing the PEL is available as the 'pel' attribute.
763
764    To create a DictWithPWL from the user's default language, use None
765    as the 'tag' argument.
766    """
767
768    _DOC_ERRORS = ["pel", "pel", "PEL", "pel"]
769
770    def __init__(self, tag, pwl=None, pel=None, broker=None):
771        """DictWithPWL constructor.
772
773        The argument 'pwl', if not None, names a file containing the
774        personal word list.  If this file does not exist, it is created
775        with default permissions.
776
777        The argument 'pel', if not None, names a file containing the personal
778        exclude list.  If this file does not exist, it is created with
779        default permissions.
780        """
781        super().__init__(tag, broker)
782        if pwl is not None:
783            if not os.path.exists(pwl):
784                f = open(pwl, "wt")
785                f.close()
786                del f
787            self.pwl = self._broker.request_pwl_dict(pwl)
788        else:
789            self.pwl = PyPWL()
790        if pel is not None:
791            if not os.path.exists(pel):
792                f = open(pel, "wt")
793                f.close()
794                del f
795            self.pel = self._broker.request_pwl_dict(pel)
796        else:
797            self.pel = PyPWL()
798
799    def _check_this(self, msg=None):
800        """Extend Dict._check_this() to check PWL validity."""
801        if self.pwl is None:
802            self._free()
803        if self.pel is None:
804            self._free()
805        super()._check_this(msg)
806        self.pwl._check_this(msg)
807        self.pel._check_this(msg)
808
809    def _free(self):
810        """Extend Dict._free() to free the PWL as well."""
811        if self.pwl is not None:
812            self.pwl._free()
813            self.pwl = None
814        if self.pel is not None:
815            self.pel._free()
816            self.pel = None
817        super()._free()
818
819    def check(self, word):
820        """Check spelling of a word.
821
822        This method takes a word in the dictionary language and returns
823        True if it is correctly spelled, and false otherwise.  It checks
824        both the dictionary and the personal word list.
825        """
826        if self.pel.check(word):
827            return False
828        if self.pwl.check(word):
829            return True
830        if super().check(word):
831            return True
832        return False
833
834    def suggest(self, word):
835        """Suggest possible spellings for a word.
836
837        This method tries to guess the correct spelling for a given
838        word, returning the possibilities in a list.
839        """
840        suggs = super().suggest(word)
841        suggs.extend([w for w in self.pwl.suggest(word) if w not in suggs])
842        for i in range(len(suggs) - 1, -1, -1):
843            if self.pel.check(suggs[i]):
844                del suggs[i]
845        return suggs
846
847    def add(self, word):
848        """Add a word to the associated personal word list.
849
850        This method adds the given word to the personal word list, and
851        automatically saves the list to disk.
852        """
853        self._check_this()
854        self.pwl.add(word)
855        self.pel.remove(word)
856
857    def remove(self, word):
858        """Add a word to the associated exclude list."""
859        self._check_this()
860        self.pwl.remove(word)
861        self.pel.add(word)
862
863    def add_to_pwl(self, word):
864        """Add a word to the associated personal word list.
865
866        This method adds the given word to the personal word list, and
867        automatically saves the list to disk.
868        """
869        self._check_this()
870        self.pwl.add_to_pwl(word)
871        self.pel.remove(word)
872
873    def is_added(self, word):
874        """Check whether a word is in the personal word list."""
875        self._check_this()
876        return self.pwl.is_added(word)
877
878    def is_removed(self, word):
879        """Check whether a word is in the personal exclude list."""
880        self._check_this()
881        return self.pel.is_added(word)
882
883
884##  Create a module-level default broker object, and make its important
885##  methods available at the module level.
886_broker = Broker()
887request_dict = _broker.request_dict
888request_pwl_dict = _broker.request_pwl_dict
889dict_exists = _broker.dict_exists
890list_dicts = _broker.list_dicts
891list_languages = _broker.list_languages
892get_param = _broker.get_param
893set_param = _broker.set_param
894
895#  Expose the "get_version" function.
896def get_enchant_version():
897    """Get the version string for the underlying enchant library."""
898    return _e.get_version().decode()
899
900
901#  Expose the "set_prefix_dir" function.
902def set_prefix_dir(path):
903    """Set the prefix used by the Enchant library to find its plugins
904
905    Called automatically when the Python library is imported when
906    required.
907    """
908    return _e.set_prefix_dir(path)
909
910    set_prefix_dir._DOC_ERRORS = ["plugins"]
911
912
913def get_user_config_dir():
914    """Return the path that will be used by some
915    Enchant providers to look for custom dictionaries.
916    """
917    return _e.get_user_config_dir().decode()
918